feat: downloads patch (#215)

This commit is contained in:
oSumAtrIX 2022-08-22 04:58:17 +02:00 committed by GitHub
parent c97611f2b1
commit b82114bddf
17 changed files with 416 additions and 61 deletions

View File

@ -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

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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()
}

View File

@ -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()
}

View File

@ -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
)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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"
)
}
}
}

View File

@ -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
}
)

View File

@ -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;")
}
)

View File

@ -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()
}
}

View File

@ -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.

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:tint="?attr/ytTextPrimary" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#ff000000" android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/>
</vector>

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:yt="http://schemas.android.com/apk/res-auto" android:id="@+id/youtube_controls_bottom_ui_container" android:layout_width="match_parent" android:layout_height="wrap_content" android:layoutDirection="ltr">
<com.google.android.libraries.youtube.common.ui.TouchImageView android:id="@+id/download_button" android:paddingLeft="12dp" android:paddingTop="22dp" android:paddingRight="12dp" android:paddingBottom="16dp" android:longClickable="false" android:layout_width="60dp" android:layout_height="60dp" android:src="@drawable/revanced_yt_download_button" android:scaleType="center" yt:layout_constraintBottom_toTopOf="@+id/quick_actions_container" yt:layout_constraintRight_toLeftOf="@+id/fullscreen_button" style="@style/YouTubePlayerButton"/>
</android.support.constraint.ConstraintLayout>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="powertube_not_installed_warning">PowerTube is not installed</string>
<string name="powertube_not_installed_notice">Please install PowerTube from https://github.com/razar-dev/PowerTube.</string>
</resources>