diff --git a/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/annotation/DownloadsCompatibility.kt b/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/annotation/DownloadsCompatibility.kt
new file mode 100644
index 000000000..75f14c119
--- /dev/null
+++ b/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/annotation/DownloadsCompatibility.kt
@@ -0,0 +1,14 @@
+package app.revanced.patches.youtube.interaction.downloads.annotation
+
+import app.revanced.patcher.annotation.Compatibility
+import app.revanced.patcher.annotation.Package
+
+@Compatibility(
+ [Package(
+ "com.google.android.youtube", arrayOf("17.27.39", "17.32.35")
+ )]
+)
+@Target(AnnotationTarget.CLASS)
+@Retention(AnnotationRetention.RUNTIME)
+internal annotation class DownloadsCompatibility
+
diff --git a/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/bytecode/patch/DownloadsBytecodePatch.kt b/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/bytecode/patch/DownloadsBytecodePatch.kt
new file mode 100644
index 000000000..57db4d2bb
--- /dev/null
+++ b/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/bytecode/patch/DownloadsBytecodePatch.kt
@@ -0,0 +1,52 @@
+package app.revanced.patches.youtube.interaction.downloads.bytecode.patch
+
+import app.revanced.patcher.annotation.Description
+import app.revanced.patcher.annotation.Name
+import app.revanced.patcher.annotation.Version
+import app.revanced.patcher.data.impl.BytecodeData
+import app.revanced.patcher.patch.PatchResult
+import app.revanced.patcher.patch.PatchResultSuccess
+import app.revanced.patcher.patch.annotations.DependsOn
+import app.revanced.patcher.patch.annotations.Patch
+import app.revanced.patcher.patch.impl.BytecodePatch
+import app.revanced.patches.youtube.interaction.downloads.annotation.DownloadsCompatibility
+import app.revanced.patches.youtube.interaction.downloads.resource.patch.DownloadsResourcePatch
+import app.revanced.patches.youtube.misc.playercontrols.bytecode.patch.PlayerControlsBytecodePatch
+import app.revanced.patches.youtube.misc.videoid.patch.VideoIdPatch
+
+@Patch
+@Name("downloads")
+@DependsOn([DownloadsResourcePatch::class, PlayerControlsBytecodePatch::class, VideoIdPatch::class])
+@Description("Enables downloading music and videos from YouTube.")
+@DownloadsCompatibility
+@Version("0.0.1")
+class DownloadsBytecodePatch : BytecodePatch() {
+ override fun execute(data: BytecodeData): PatchResult {
+ val integrationsPackage = "app/revanced/integrations"
+ val classDescriptor = "L$integrationsPackage/videoplayer/DownloadButton;"
+
+ /*
+ initialize the control
+ */
+
+ val initializeDownloadsDescriptor = "$classDescriptor->initializeDownloadButton(Ljava/lang/Object;)V"
+ PlayerControlsBytecodePatch.initializeControl(initializeDownloadsDescriptor)
+
+ /*
+ add code to change the visibility of the control
+ */
+
+ val changeVisibilityDescriptor = "$classDescriptor->changeVisibility(Z)V"
+ PlayerControlsBytecodePatch.injectVisibilityCheckCall(changeVisibilityDescriptor)
+
+ /*
+ add code to change to update the video id
+ */
+
+ val setVideoIdDescriptor =
+ "L$integrationsPackage/patches/downloads/DownloadsPatch;->setVideoId(Ljava/lang/String;)V"
+ VideoIdPatch.injectCall(setVideoIdDescriptor)
+
+ return PatchResultSuccess()
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/resource/patch/DownloadsResourcePatch.kt b/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/resource/patch/DownloadsResourcePatch.kt
new file mode 100644
index 000000000..719efbb5b
--- /dev/null
+++ b/src/main/kotlin/app/revanced/patches/youtube/interaction/downloads/resource/patch/DownloadsResourcePatch.kt
@@ -0,0 +1,56 @@
+package app.revanced.patches.youtube.interaction.downloads.resource.patch
+
+import app.revanced.patcher.annotation.Description
+import app.revanced.patcher.annotation.Name
+import app.revanced.patcher.annotation.Version
+import app.revanced.patcher.data.impl.ResourceData
+import app.revanced.patcher.patch.PatchResult
+import app.revanced.patcher.patch.PatchResultSuccess
+import app.revanced.patcher.patch.annotations.DependsOn
+import app.revanced.patcher.patch.impl.ResourcePatch
+import app.revanced.patches.youtube.interaction.downloads.annotation.DownloadsCompatibility
+import app.revanced.patches.youtube.misc.manifest.patch.FixLocaleConfigErrorPatch
+import app.revanced.patches.youtube.misc.playercontrols.resource.patch.BottomControlsResourcePatch
+import app.revanced.patches.youtube.misc.settings.bytecode.patch.SettingsPatch
+import app.revanced.patches.youtube.misc.settings.framework.components.impl.StringResource
+import app.revanced.patches.youtube.misc.settings.framework.components.impl.SwitchPreference
+import app.revanced.util.resources.ResourceUtils
+import app.revanced.util.resources.ResourceUtils.Settings.mergeStrings
+import app.revanced.util.resources.ResourceUtils.copyResources
+
+@Name("downloads-resource-patch")
+@DependsOn([BottomControlsResourcePatch::class, FixLocaleConfigErrorPatch::class, SettingsPatch::class])
+@Description("Makes necessary changes to resources for the download button.")
+@DownloadsCompatibility
+@Version("0.0.1")
+class DownloadsResourcePatch : ResourcePatch() {
+ override fun execute(data: ResourceData): PatchResult {
+ SettingsPatch.PreferenceScreen.LAYOUT.addPreferences(SwitchPreference(
+ "revanced_downloads",
+ StringResource("revanced_downloads_enabled_title", "Show download button"),
+ true,
+ StringResource("revanced_downloads_enabled_summary_on", "Download button is visible"),
+ StringResource("revanced_downloads_enabled_summary_off", "Download button is hidden")
+ ))
+
+ /*
+ * Copy strings
+ */
+
+ data.mergeStrings("downloads/host/values/strings.xml")
+
+ /*
+ * Copy resources
+ */
+
+ data.copyResources("downloads", ResourceUtils.ResourceGroup("drawable", "revanced_yt_download_button.xml"))
+
+ /*
+ * Add download button node
+ */
+
+ BottomControlsResourcePatch.addControls("downloads/host/layout/${BottomControlsResourcePatch.TARGET_RESOURCE_NAME}")
+
+ return PatchResultSuccess()
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/patch/resource/SwipeControlsResourcePatch.kt b/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/patch/resource/SwipeControlsResourcePatch.kt
index 05d732e04..84f8fb4e3 100644
--- a/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/patch/resource/SwipeControlsResourcePatch.kt
+++ b/src/main/kotlin/app/revanced/patches/youtube/interaction/swipecontrols/patch/resource/SwipeControlsResourcePatch.kt
@@ -1,6 +1,5 @@
package app.revanced.patches.youtube.interaction.swipecontrols.patch.resource
-import app.revanced.extensions.injectResources
import app.revanced.patcher.annotation.Name
import app.revanced.patcher.annotation.Version
import app.revanced.patcher.data.impl.ResourceData
@@ -11,6 +10,8 @@ import app.revanced.patcher.patch.impl.ResourcePatch
import app.revanced.patches.youtube.interaction.swipecontrols.annotation.SwipeControlsCompatibility
import app.revanced.patches.youtube.misc.settings.bytecode.patch.SettingsPatch
import app.revanced.patches.youtube.misc.settings.framework.components.impl.*
+import app.revanced.util.resources.ResourceUtils
+import app.revanced.util.resources.ResourceUtils.copyResources
@Name("swipe-controls-resource-patch")
@DependsOn([SettingsPatch::class])
@@ -93,16 +94,15 @@ class SwipeControlsResourcePatch : ResourcePatch() {
val resourcesDir = "swipecontrols"
- data.injectResources(
- this.javaClass.classLoader,
- resourcesDir,
- "drawable",
- listOf(
- "ic_sc_brightness_auto",
- "ic_sc_brightness_manual",
- "ic_sc_volume_mute",
- "ic_sc_volume_normal"
- ).map { "$it.xml" }
+ data.copyResources(
+ "swipecontrols",
+ ResourceUtils.ResourceGroup(
+ "drawable",
+ "ic_sc_brightness_auto.xml",
+ "ic_sc_brightness_manual.xml",
+ "ic_sc_volume_mute.xml",
+ "ic_sc_volume_normal.xml"
+ )
)
return PatchResultSuccess()
}
diff --git a/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/resource/patch/ReturnYouTubeDislikeResourcePatch.kt b/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/resource/patch/ReturnYouTubeDislikeResourcePatch.kt
index a229d7d38..220dd657b 100644
--- a/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/resource/patch/ReturnYouTubeDislikeResourcePatch.kt
+++ b/src/main/kotlin/app/revanced/patches/youtube/layout/returnyoutubedislike/resource/patch/ReturnYouTubeDislikeResourcePatch.kt
@@ -13,7 +13,7 @@ import app.revanced.patches.youtube.misc.manifest.patch.FixLocaleConfigErrorPatc
import app.revanced.patches.youtube.misc.settings.bytecode.patch.SettingsPatch
import app.revanced.patches.youtube.misc.settings.framework.components.impl.Preference
import app.revanced.patches.youtube.misc.settings.framework.components.impl.StringResource
-import app.revanced.util.resources.ResourceUtils.iterateXmlNodeChildren
+import app.revanced.util.resources.ResourceUtils.Settings.mergeStrings
@DependsOn([FixLocaleConfigErrorPatch::class, SettingsPatch::class])
@Name("return-youtube-dislike-resource-patch")
@@ -35,12 +35,7 @@ class ReturnYouTubeDislikeResourcePatch : ResourcePatch() {
)
)
// merge strings
- data.iterateXmlNodeChildren("returnyoutubedislike/host/values/strings.xml", "resources") {
- // TODO: figure out why this is needed
- if (!it.hasAttributes()) return@iterateXmlNodeChildren
- val attributes = it.attributes
- SettingsPatch.addString(attributes.getNamedItem("name")!!.nodeValue!!, it.textContent!!)
- }
+ data.mergeStrings("returnyoutubedislike/host/values/strings.xml")
return PatchResultSuccess()
}
diff --git a/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock/bytecode/fingerprints/ShowPlayerControlsFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock/bytecode/fingerprints/ShowPlayerControlsFingerprint.kt
deleted file mode 100644
index 39c330aee..000000000
--- a/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock/bytecode/fingerprints/ShowPlayerControlsFingerprint.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-package app.revanced.patches.youtube.layout.sponsorblock.bytecode.fingerprints
-
-import app.revanced.patcher.annotation.Name
-import app.revanced.patcher.annotation.Version
-import app.revanced.patcher.fingerprint.method.annotation.DirectPatternScanMethod
-import app.revanced.patcher.fingerprint.method.annotation.MatchingMethod
-import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
-import app.revanced.patches.youtube.layout.sponsorblock.annotations.SponsorBlockCompatibility
-
-@Name("show-player-controls-fingerprint")
-@MatchingMethod(
- "LYouTubeControlsOverlay;", "ac"
-)
-@DirectPatternScanMethod
-@SponsorBlockCompatibility
-@Version("0.0.1")
-object ShowPlayerControlsFingerprint : MethodFingerprint(
- "V", null, listOf("Z","Z"), null, null
-)
\ No newline at end of file
diff --git a/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock/bytecode/patch/SponsorBlockBytecodePatch.kt b/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock/bytecode/patch/SponsorBlockBytecodePatch.kt
index 3ba5406e6..c2ff0764c 100644
--- a/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock/bytecode/patch/SponsorBlockBytecodePatch.kt
+++ b/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock/bytecode/patch/SponsorBlockBytecodePatch.kt
@@ -23,6 +23,7 @@ import app.revanced.patches.youtube.layout.sponsorblock.bytecode.fingerprints.*
import app.revanced.patches.youtube.layout.sponsorblock.resource.patch.SponsorBlockResourcePatch
import app.revanced.patches.youtube.misc.integrations.patch.IntegrationsPatch
import app.revanced.patches.youtube.misc.mapping.patch.ResourceIdMappingProviderResourcePatch
+import app.revanced.patches.youtube.misc.playercontrols.bytecode.patch.PlayerControlsBytecodePatch
import app.revanced.patches.youtube.misc.videoid.patch.VideoIdPatch
import org.jf.dexlib2.AccessFlags
import org.jf.dexlib2.Opcode
@@ -38,7 +39,7 @@ import org.jf.dexlib2.util.MethodUtil
@Patch
@DependsOn(
- dependencies = [IntegrationsPatch::class, ResourceIdMappingProviderResourcePatch::class, SponsorBlockResourcePatch::class, VideoIdPatch::class]
+ dependencies = [PlayerControlsBytecodePatch::class, IntegrationsPatch::class, ResourceIdMappingProviderResourcePatch::class, SponsorBlockResourcePatch::class, VideoIdPatch::class]
)
@Name("sponsorblock")
@Description("Integrate SponsorBlock.")
@@ -171,8 +172,7 @@ class SponsorBlockBytecodePatch : BytecodePatch(
/*
Voting & Shield button
*/
- ShowPlayerControlsFingerprint.resolve(data, data.classes.find { it.type.endsWith("YouTubeControlsOverlay;") }!!)
- val controlsMethodResult = ShowPlayerControlsFingerprint.result!!
+ val controlsMethodResult = PlayerControlsBytecodePatch.showPlayerControlsFingerprintResult
val controlsLayoutStubResourceId =
ResourceIdMappingProviderResourcePatch.resourceMappings.single { it.type == "id" && it.name == "controls_layout_stub" }.id
@@ -217,12 +217,8 @@ class SponsorBlockBytecodePatch : BytecodePatch(
}
// change visibility of the buttons
- controlsMethodResult.mutableMethod.addInstructions(
- 0, """
- invoke-static {p1}, Lapp/revanced/integrations/sponsorblock/ShieldButton;->changeVisibility(Z)V
- invoke-static {p1}, Lapp/revanced/integrations/sponsorblock/VotingButton;->changeVisibility(Z)V
- """.trimIndent()
- )
+ PlayerControlsBytecodePatch.injectVisibilityCheckCall("Lapp/revanced/integrations/sponsorblock/ShieldButton;->changeVisibility(Z)V")
+ PlayerControlsBytecodePatch.injectVisibilityCheckCall("Lapp/revanced/integrations/sponsorblock/VotingButton;->changeVisibility(Z)V")
// set SegmentHelperLayout.context to the player layout instance
val instanceRegister = 0
diff --git a/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock/resource/patch/SponsorBlockResourcePatch.kt b/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock/resource/patch/SponsorBlockResourcePatch.kt
index b2d1c50f6..cad7b7962 100644
--- a/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock/resource/patch/SponsorBlockResourcePatch.kt
+++ b/src/main/kotlin/app/revanced/patches/youtube/layout/sponsorblock/resource/patch/SponsorBlockResourcePatch.kt
@@ -13,9 +13,9 @@ import app.revanced.patches.youtube.misc.settings.bytecode.patch.SettingsPatch
import app.revanced.patches.youtube.misc.settings.framework.components.impl.Preference
import app.revanced.patches.youtube.misc.settings.framework.components.impl.StringResource
import app.revanced.util.resources.ResourceUtils
+import app.revanced.util.resources.ResourceUtils.Settings.mergeStrings
import app.revanced.util.resources.ResourceUtils.copyResources
import app.revanced.util.resources.ResourceUtils.copyXmlNode
-import app.revanced.util.resources.ResourceUtils.iterateXmlNodeChildren
@Name("sponsorblock-resource-patch")
@SponsorBlockCompatibility
@@ -40,20 +40,7 @@ class SponsorBlockResourcePatch : ResourcePatch() {
/*
merge SponsorBlock strings to main strings
*/
- data.iterateXmlNodeChildren("sponsorblock/host/values/strings.xml", "resources") {
- // TODO: figure out why this is needed
- if (!it.hasAttributes()) return@iterateXmlNodeChildren
-
- val attributes = it.attributes
- val key = attributes.getNamedItem("name")!!.nodeValue!!
- val value = it.textContent!!
-
- // all strings of SponsorBlock which have this attribute have the attribute value false,
- // hence a null check suffices
- val formatted = attributes.getNamedItem("formatted") == null
-
- SettingsPatch.addString(key, value, formatted)
- }
+ data.mergeStrings("sponsorblock/host/values/strings.xml")
/*
merge SponsorBlock drawables to main drawables
diff --git a/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/annotation/PlayerControlsCompatibility.kt b/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/annotation/PlayerControlsCompatibility.kt
new file mode 100644
index 000000000..4f00f6030
--- /dev/null
+++ b/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/annotation/PlayerControlsCompatibility.kt
@@ -0,0 +1,13 @@
+package app.revanced.patches.youtube.misc.playercontrols.annotation
+
+import app.revanced.patcher.annotation.Compatibility
+import app.revanced.patcher.annotation.Package
+
+@Compatibility(
+ [Package(
+ "com.google.android.youtube", arrayOf("17.27.39", "17.32.35")
+ )]
+)
+@Target(AnnotationTarget.CLASS)
+@Retention(AnnotationRetention.RUNTIME)
+internal annotation class PlayerControlsCompatibility
\ No newline at end of file
diff --git a/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/bytecode/patch/PlayerControlsBytecodePatch.kt b/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/bytecode/patch/PlayerControlsBytecodePatch.kt
new file mode 100644
index 000000000..e629ecbfc
--- /dev/null
+++ b/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/bytecode/patch/PlayerControlsBytecodePatch.kt
@@ -0,0 +1,83 @@
+package app.revanced.patches.youtube.misc.playercontrols.bytecode.patch
+
+import app.revanced.patcher.annotation.Description
+import app.revanced.patcher.annotation.Name
+import app.revanced.patcher.annotation.Version
+import app.revanced.patcher.data.impl.BytecodeData
+import app.revanced.patcher.extensions.addInstruction
+import app.revanced.patcher.fingerprint.method.impl.MethodFingerprintResult
+import app.revanced.patcher.fingerprint.method.utils.MethodFingerprintUtils.resolve
+import app.revanced.patcher.patch.PatchResult
+import app.revanced.patcher.patch.PatchResultSuccess
+import app.revanced.patcher.patch.annotations.DependsOn
+import app.revanced.patcher.patch.impl.BytecodePatch
+import app.revanced.patches.youtube.misc.mapping.patch.ResourceIdMappingProviderResourcePatch
+import app.revanced.patches.youtube.misc.playercontrols.annotation.PlayerControlsCompatibility
+import app.revanced.patches.youtube.misc.playercontrols.fingerprints.BottomControlsInflateFingerprint
+import app.revanced.patches.youtube.misc.playercontrols.fingerprints.PlayerControlsVisibilityFingerprint
+import org.jf.dexlib2.iface.instruction.OneRegisterInstruction
+
+@Name("player-controls-bytecode-patch")
+@DependsOn([ResourceIdMappingProviderResourcePatch::class])
+@Description("Manages the code for the player controls of the YouTube player.")
+@PlayerControlsCompatibility
+@Version("0.0.1")
+class PlayerControlsBytecodePatch : BytecodePatch(
+ listOf(PlayerControlsVisibilityFingerprint)
+) {
+ override fun execute(data: BytecodeData): PatchResult {
+ showPlayerControlsFingerprintResult = PlayerControlsVisibilityFingerprint.result!!
+
+ bottomUiContainerResourceId = ResourceIdMappingProviderResourcePatch
+ .resourceMappings
+ .single { it.type == "id" && it.name == "bottom_ui_container_stub" }.id
+
+ // TODO: another solution is required, this is hacky
+ listOf(BottomControlsInflateFingerprint).resolve(data, data.classes)
+ inflateFingerprintResult = BottomControlsInflateFingerprint.result!!
+
+ return PatchResultSuccess()
+ }
+
+ internal companion object {
+ var bottomUiContainerResourceId: Long = 0
+
+ lateinit var showPlayerControlsFingerprintResult: MethodFingerprintResult
+
+ private var inflateFingerprintResult: MethodFingerprintResult? = null
+ set(fingerprint) {
+ field = fingerprint!!.also {
+ moveToRegisterInstructionIndex = it.patternScanResult!!.endIndex
+ viewRegister =
+ (it.mutableMethod.implementation!!.instructions[moveToRegisterInstructionIndex] as OneRegisterInstruction).registerA
+ }
+ }
+
+ private var moveToRegisterInstructionIndex: Int = 0
+ private var viewRegister: Int = 0
+
+ /**
+ * Injects the code to change the visibility of controls.
+ * @param descriptor The descriptor of the method which should be called.
+ */
+ fun injectVisibilityCheckCall(descriptor: String) {
+ showPlayerControlsFingerprintResult.mutableMethod.addInstruction(
+ 0,
+ """
+ invoke-static {p1}, $descriptor
+ """
+ )
+ }
+
+ /**
+ * Injects the code to initialize the controls.
+ * @param descriptor The descriptor of the method which should be calleed.
+ */
+ fun initializeControl(descriptor: String) {
+ inflateFingerprintResult!!.mutableMethod.addInstruction(
+ moveToRegisterInstructionIndex + 1,
+ "invoke-static {v$viewRegister}, $descriptor"
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/fingerprints/BottomControlsInflateFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/fingerprints/BottomControlsInflateFingerprint.kt
new file mode 100644
index 000000000..ee86a246f
--- /dev/null
+++ b/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/fingerprints/BottomControlsInflateFingerprint.kt
@@ -0,0 +1,31 @@
+package app.revanced.patches.youtube.misc.playercontrols.fingerprints
+
+import app.revanced.patcher.annotation.Name
+import app.revanced.patcher.annotation.Version
+import app.revanced.patcher.fingerprint.method.annotation.DirectPatternScanMethod
+import app.revanced.patcher.fingerprint.method.annotation.MatchingMethod
+import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
+import app.revanced.patches.youtube.misc.playercontrols.annotation.PlayerControlsCompatibility
+import app.revanced.patches.youtube.misc.playercontrols.bytecode.patch.PlayerControlsBytecodePatch
+import org.jf.dexlib2.Opcode
+import org.jf.dexlib2.iface.instruction.WideLiteralInstruction
+
+@Name("bottom-controls-inflate-fingerprint")
+@MatchingMethod(
+ "Lknf;", "a"
+)
+@DirectPatternScanMethod
+@PlayerControlsCompatibility
+@Version("0.0.1")
+object BottomControlsInflateFingerprint : MethodFingerprint(
+ null, null, null, listOf(
+ Opcode.CHECK_CAST,
+ Opcode.INVOKE_VIRTUAL,
+ Opcode.MOVE_RESULT_OBJECT
+ ), null,
+ { methodDef ->
+ methodDef.implementation?.instructions?.any { instruction ->
+ (instruction as? WideLiteralInstruction)?.wideLiteral == PlayerControlsBytecodePatch.bottomUiContainerResourceId
+ } == true
+ }
+)
\ No newline at end of file
diff --git a/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/fingerprints/PlayerControlsVisibilityFingerprint.kt b/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/fingerprints/PlayerControlsVisibilityFingerprint.kt
new file mode 100644
index 000000000..e3500a1bd
--- /dev/null
+++ b/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/fingerprints/PlayerControlsVisibilityFingerprint.kt
@@ -0,0 +1,21 @@
+package app.revanced.patches.youtube.misc.playercontrols.fingerprints
+
+import app.revanced.patcher.annotation.Name
+import app.revanced.patcher.annotation.Version
+import app.revanced.patcher.fingerprint.method.annotation.DirectPatternScanMethod
+import app.revanced.patcher.fingerprint.method.annotation.MatchingMethod
+import app.revanced.patcher.fingerprint.method.impl.MethodFingerprint
+import app.revanced.patches.youtube.misc.playercontrols.annotation.PlayerControlsCompatibility
+
+@Name("player-controls-visibility-fingerprint")
+@MatchingMethod(
+ "LYouTubeControlsOverlay;", "ag"
+)
+@DirectPatternScanMethod
+@PlayerControlsCompatibility
+@Version("0.0.1")
+object PlayerControlsVisibilityFingerprint : MethodFingerprint(
+ "V", null, listOf("Z", "Z"), null, null, { methodDef ->
+ methodDef.definingClass.endsWith("YouTubeControlsOverlay;")
+ }
+)
\ No newline at end of file
diff --git a/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/resource/patch/BottomControlsResourcePatch.kt b/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/resource/patch/BottomControlsResourcePatch.kt
new file mode 100644
index 000000000..b6a51c8d6
--- /dev/null
+++ b/src/main/kotlin/app/revanced/patches/youtube/misc/playercontrols/resource/patch/BottomControlsResourcePatch.kt
@@ -0,0 +1,86 @@
+package app.revanced.patches.youtube.misc.playercontrols.resource.patch
+
+import app.revanced.patcher.annotation.Description
+import app.revanced.patcher.annotation.Name
+import app.revanced.patcher.annotation.Version
+import app.revanced.patcher.data.impl.DomFileEditor
+import app.revanced.patcher.data.impl.ResourceData
+import app.revanced.patcher.patch.PatchResult
+import app.revanced.patcher.patch.PatchResultSuccess
+import app.revanced.patcher.patch.annotations.DependsOn
+import app.revanced.patcher.patch.impl.ResourcePatch
+import app.revanced.patches.youtube.misc.manifest.patch.FixLocaleConfigErrorPatch
+import app.revanced.patches.youtube.misc.playercontrols.annotation.PlayerControlsCompatibility
+import java.io.Closeable
+
+@Name("bottom-controls-resource-patch")
+@DependsOn([FixLocaleConfigErrorPatch::class])
+@Description("Manages the resources for the bottom controls of the YouTube player.")
+@PlayerControlsCompatibility
+@Version("0.0.1")
+class BottomControlsResourcePatch : ResourcePatch(), Closeable {
+ override fun execute(data: ResourceData): PatchResult {
+ resourceData = data
+ targetXmlEditor = data.xmlEditor[TARGET_RESOURCE]
+
+ return PatchResultSuccess()
+ }
+
+ companion object {
+ internal const val TARGET_RESOURCE_NAME = "youtube_controls_bottom_ui_container.xml"
+ private const val TARGET_RESOURCE = "res/layout/$TARGET_RESOURCE_NAME"
+
+ private lateinit var resourceData: ResourceData
+ private lateinit var targetXmlEditor: DomFileEditor
+
+ // The element to which to add the new elements to
+ private var lastLeftOf = "fullscreen_button"
+
+
+
+ /**
+ * Add new controls to the bottom of the YouTube player.
+ * @param hostYouTubeControlsBottomUiResourceName The hosting resource name containing the elements.
+ */
+ internal fun addControls(hostYouTubeControlsBottomUiResourceName: String) {
+ val sourceXmlEditor =
+ resourceData.xmlEditor[this::class.java.classLoader.getResourceAsStream(
+ hostYouTubeControlsBottomUiResourceName
+ )!!]
+
+ val targetElement =
+ "android.support.constraint.ConstraintLayout"
+
+ val hostElements = sourceXmlEditor.file.getElementsByTagName(targetElement).item(0).childNodes
+
+ val destinationResourceFile = this.targetXmlEditor.file
+ val destinationElement =
+ destinationResourceFile.getElementsByTagName(targetElement).item(0)
+
+ for (index in 1 until hostElements.length) {
+ val element = hostElements.item(index).cloneNode(true)
+
+ // if the element has no attributes theres no point to adding it to the destination
+ if (!element.hasAttributes()) continue
+
+ // set the elements lastLeftOf attribute to the lastLeftOf value
+ val namespace = "@+id"
+ element.attributes.getNamedItem("yt:layout_constraintRight_toLeftOf").nodeValue =
+ "$namespace/$lastLeftOf"
+
+ // set lastLeftOf attribute to the the current element
+ val nameSpaceLength = 4
+ lastLeftOf = element.attributes.getNamedItem("android:id").nodeValue.substring(nameSpaceLength)
+
+ // copy the element
+ destinationResourceFile.adoptNode(element)
+ destinationElement.appendChild(element)
+ }
+ sourceXmlEditor.close()
+ }
+ }
+
+ override fun close() {
+ targetXmlEditor.close()
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/app/revanced/util/resources/ResourceUtils.kt b/src/main/kotlin/app/revanced/util/resources/ResourceUtils.kt
index 301bad2c4..167375a18 100644
--- a/src/main/kotlin/app/revanced/util/resources/ResourceUtils.kt
+++ b/src/main/kotlin/app/revanced/util/resources/ResourceUtils.kt
@@ -2,11 +2,38 @@ package app.revanced.util.resources
import app.revanced.patcher.data.impl.DomFileEditor
import app.revanced.patcher.data.impl.ResourceData
+import app.revanced.patches.youtube.misc.settings.bytecode.patch.SettingsPatch
+import app.revanced.patches.youtube.misc.settings.framework.components.impl.StringResource
import org.w3c.dom.Node
import java.nio.file.Files
import java.nio.file.StandardCopyOption
internal object ResourceUtils {
+
+ /**
+ * Settings related utilities
+ */
+ internal object Settings {
+ /**
+ * Merge strings. This handles [StringResource]s automatically.
+ * @param host The hosting xml resource. Needs to be a valid strings.xml resource.
+ */
+ internal fun ResourceData.mergeStrings(host: String) {
+ this.iterateXmlNodeChildren(host, "resources") {
+ // TODO: figure out why this is needed
+ if (!it.hasAttributes()) return@iterateXmlNodeChildren
+
+ val attributes = it.attributes
+ val key = attributes.getNamedItem("name")!!.nodeValue!!
+ val value = it.textContent!!
+
+ val formatted = attributes.getNamedItem("formatted") == null
+
+ SettingsPatch.addString(key, value, formatted)
+ }
+ }
+ }
+
/**
* Copy resources from the current class loader to the resource directory.
* @param sourceResourceDirectory The source resource directory name.
diff --git a/src/main/resources/downloads/drawable/revanced_yt_download_button.xml b/src/main/resources/downloads/drawable/revanced_yt_download_button.xml
new file mode 100644
index 000000000..44ef4b6aa
--- /dev/null
+++ b/src/main/resources/downloads/drawable/revanced_yt_download_button.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/downloads/host/layout/youtube_controls_bottom_ui_container.xml b/src/main/resources/downloads/host/layout/youtube_controls_bottom_ui_container.xml
new file mode 100644
index 000000000..23254785a
--- /dev/null
+++ b/src/main/resources/downloads/host/layout/youtube_controls_bottom_ui_container.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/src/main/resources/downloads/host/values/strings.xml b/src/main/resources/downloads/host/values/strings.xml
new file mode 100644
index 000000000..afcde12a2
--- /dev/null
+++ b/src/main/resources/downloads/host/values/strings.xml
@@ -0,0 +1,5 @@
+
+
+ PowerTube is not installed
+ Please install PowerTube from https://github.com/razar-dev/PowerTube.
+