feat(YouTube Music): Add Change header patch

Co-authored-by: ILoveOpenSourceApplications <117499019+iloveopensourceapplications@users.noreply.github.com>
This commit is contained in:
oSumAtrIX 2026-03-08 02:07:34 +01:00
parent f10f5e2910
commit f22ea5507d
No known key found for this signature in database
GPG key ID: A9B3094ACDB604B4
9 changed files with 351 additions and 192 deletions

View file

@ -0,0 +1,37 @@
package app.revanced.extension.music.patches;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.ResourceType;
import app.revanced.extension.shared.Utils;
import app.revanced.extension.music.settings.Settings;
public class ChangeHeaderPatch {
public enum HeaderLogo {
DEFAULT(null),
REVANCED("revanced_header_dark"),
CUSTOM("revanced_header_custom_dark");
private final String drawableName;
HeaderLogo(String drawableName) {
this.drawableName = drawableName;
}
private Integer getDrawableId() {
if (drawableName == null) {
return null;
}
int id = Utils.getResourceIdentifier(ResourceType.DRAWABLE, drawableName);
if (id == 0) {
Logger.printException(() ->
"Header drawable not found: " + drawableName
);
Settings.HEADER_LOGO.resetToDefault();
return null;
}
return id;
}
}
}

View file

@ -2,6 +2,7 @@ package app.revanced.extension.music.settings;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static app.revanced.extension.music.patches.ChangeHeaderPatch.*;
import static app.revanced.extension.shared.settings.Setting.parent;
import app.revanced.extension.shared.settings.YouTubeAndMusicSettings;
@ -28,6 +29,7 @@ public class Settings extends YouTubeAndMusicSettings {
public static final BooleanSetting HIDE_NAVIGATION_BAR_UPGRADE_BUTTON = new BooleanSetting("revanced_music_hide_navigation_bar_upgrade_button", TRUE, true);
public static final BooleanSetting HIDE_NAVIGATION_BAR = new BooleanSetting("revanced_music_hide_navigation_bar", FALSE, true);
public static final BooleanSetting HIDE_NAVIGATION_BAR_LABEL = new BooleanSetting("revanced_music_hide_navigation_bar_labels", FALSE, true);
public static final EnumSetting<HeaderLogo> HEADER_LOGO = new EnumSetting<>("revnaced_header_logo", HeaderLogo.DEFAULT, true);
// Player
public static final BooleanSetting CHANGE_MINIPLAYER_COLOR = new BooleanSetting("revanced_music_change_miniplayer_color", FALSE, true);

View file

@ -11,4 +11,5 @@ public class YouTubeAndMusicSettings extends BaseSettings {
// Miscellaneous
public static final BooleanSetting DEBUG_PROTOBUFFER = new BooleanSetting("revanced_debug_protobuffer", FALSE, false,
"revanced_debug_protobuffer_user_dialog_message", parent(BaseSettings.DEBUG));
}

View file

@ -65,7 +65,6 @@ val customBrandingPatch = baseCustomBrandingPatch(
mainActivityName = MUSIC_MAIN_ACTIVITY_NAME,
activityAliasNameWithIntents = MUSIC_MAIN_ACTIVITY_NAME,
preferenceScreen = PreferenceScreen.GENERAL,
block = {
dependsOn(sharedExtensionPatch, disableSplashAnimationPatch)

View file

@ -0,0 +1,75 @@
package app.revanced.patches.music.layout.branding.header
import app.revanced.patcher.extensions.addInstructions
import app.revanced.patcher.extensions.getInstruction
import app.revanced.patcher.extensions.wideLiteral
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patches.all.misc.resources.addResources
import app.revanced.patches.music.misc.settings.PreferenceScreen
import app.revanced.patches.shared.layout.branding.header.changeHeaderPatch
import app.revanced.patches.shared.misc.mapping.ResourceType
import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
import app.revanced.util.forEachInstructionAsSequence
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
private val targetResourceDirectoryNames = mapOf(
"drawable-hdpi" to "121x36 px",
"drawable-xhdpi" to "160x48 px",
"drawable-xxhdpi" to "240x72 px",
"drawable-xxxhdpi" to "320x96 px"
)
private val variants = arrayOf("dark")
private val logoResourceNames = arrayOf("revanced_header_dark")
private val headerDrawableNames = arrayOf(
"action_bar_logo_ringo2",
"ytm_logo_ringo2"
)
private const val EXTENSION_CLASS_DESCRIPTOR =
"Lapp/revanced/extension/music/patches/ChangeHeaderPatch;"
private val changeHeaderBytecodePatch = bytecodePatch {
dependsOn(resourceMappingPatch)
apply {
headerDrawableNames.forEach { drawableName ->
val drawableId = ResourceType.DRAWABLE[drawableName]
forEachInstructionAsSequence({ _, method, instruction, index ->
if (instruction.wideLiteral != drawableId) return@forEachInstructionAsSequence null
val register = method.getInstruction<OneRegisterInstruction>(index).registerA
return@forEachInstructionAsSequence index to register
}) { method, (index, register) ->
method.addInstructions(
index + 1,
"""
invoke-static { v$register }, ${EXTENSION_CLASS_DESCRIPTOR}->getHeaderDrawableId(I)I
move-result v$register
""",
)
}
}
}
}
@Suppress("unused")
val changeHeaderPatch = changeHeaderPatch(
targetResourceDirectoryNames = targetResourceDirectoryNames,
changeHeaderBytecodePatch = changeHeaderBytecodePatch,
logoResourceNames = logoResourceNames,
variants = variants,
preferenceScreen = PreferenceScreen.GENERAL,
compatiblePackages = arrayOf(
"com.google.android.apps.youtube.music" to setOf(
"7.29.52",
"8.10.52",
"8.37.56",
"8.40.54",
),
),
resourcesAppId = "music",
)

View file

@ -0,0 +1,160 @@
package app.revanced.patches.shared.layout.branding.header
import app.revanced.patcher.patch.Package
import app.revanced.patcher.patch.Patch
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.ResourcePatchContext
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.settings.preference.BasePreferenceScreen
import app.revanced.patches.shared.misc.settings.preference.ListPreference
import app.revanced.util.ResourceGroup
import app.revanced.util.Utils.trimIndentMultiline
import app.revanced.util.copyResources
import java.io.File
internal const val CUSTOM_HEADER_RESOURCE_NAME = "revanced_header_custom"
@Suppress("unused")
fun changeHeaderPatch(
targetResourceDirectoryNames: Map<String, String>,
changeHeaderBytecodePatch: Patch,
vararg compatiblePackages: Package,
variants: Array<String>,
logoResourceNames: Array<String>,
preferenceScreen: BasePreferenceScreen.Screen,
resourcesAppId: String,
applyBlock: ResourcePatchContext.() -> Unit = {},
): Patch {
val customHeaderResourceFileNames = variants.map { variant ->
"${CUSTOM_HEADER_RESOURCE_NAME}_$variant.png"
}.toTypedArray()
return resourcePatch(
name = "Change header",
description = "Adds an option to change the header logo in the top left corner of the app.",
) {
dependsOn(addResourcesPatch, changeHeaderBytecodePatch)
compatibleWith(packages = compatiblePackages)
val custom by stringOption(
name = "Custom header logo",
description = """
Folder with images to use as a custom header logo.
The folder must contain one or more of the following folders, depending on the DPI of the device:
${targetResourceDirectoryNames.keys.joinToString("\n") { "- $it" }}
Each of the folders must contain all of the following files:
${customHeaderResourceFileNames.joinToString("\n")}
The image dimensions must be as follows:
${targetResourceDirectoryNames.map { (dpi, dim) -> "- $dpi: $dim" }.joinToString("\n")}
""".trimIndentMultiline(),
)
apply {
addResources(resourcesAppId, "layout.branding.header.changeHeaderPatch")
preferenceScreen.addPreferences(
if (custom == null) {
ListPreference("revanced_header_logo")
} else {
ListPreference(
key = "revanced_header_logo",
entriesKey = "revanced_header_logo_custom_entries",
entryValuesKey = "revanced_header_logo_custom_entry_values",
)
},
)
logoResourceNames.forEach { logo ->
variants.forEach { variant ->
copyResources(
"change-header",
ResourceGroup(
"drawable",
logo + "_" + variant + ".xml",
),
)
}
}
// Copy custom template. Images are only used if settings
// are imported and a custom header is enabled.
targetResourceDirectoryNames.keys.forEach { dpi ->
variants.forEach { variant ->
copyResources(
"change-header",
ResourceGroup(
dpi,
resources = customHeaderResourceFileNames,
),
)
}
}
applyBlock()
// Copy user provided images last, so if an exception is thrown due to bad input.
if (custom != null) {
val customFile = File(custom!!.trim())
if (!customFile.exists()) {
throw PatchException(
"The custom header path cannot be found: " +
customFile.absolutePath,
)
}
if (!customFile.isDirectory) {
throw PatchException(
"The custom header path must be a folder: " +
customFile.absolutePath,
)
}
var copiedFiles = false
// For each source folder, copy the files to the target resource directories.
customFile.listFiles { file ->
file.isDirectory && file.name in targetResourceDirectoryNames
}!!.forEach { dpiSourceFolder ->
val targetDpiFolder = get("res").resolve(dpiSourceFolder.name)
if (!targetDpiFolder.exists()) {
// Should never happen.
throw IllegalStateException("Resource not found: $dpiSourceFolder")
}
val customFiles = dpiSourceFolder.listFiles { file ->
file.isFile && file.name in customHeaderResourceFileNames
}!!
if (customFiles.isNotEmpty() && customFiles.size != variants.size) {
throw PatchException(
"Both light/dark mode images " +
"must be specified but only found: " + customFiles.map { it.name },
)
}
customFiles.forEach { imgSourceFile ->
val imgTargetFile = targetDpiFolder.resolve(imgSourceFile.name)
imgSourceFile.copyTo(target = imgTargetFile, overwrite = true)
copiedFiles = true
}
}
if (!copiedFiles) {
throw PatchException(
"Expected to find directories and files: " +
customHeaderResourceFileNames.contentToString() +
"\nBut none were found in the provided option file path: " + customFile.absolutePath,
)
}
}
}
}
}

View file

@ -3,27 +3,26 @@ package app.revanced.patches.youtube.layout.branding.header
import app.revanced.patcher.extensions.addInstructions
import app.revanced.patcher.extensions.getInstruction
import app.revanced.patcher.extensions.wideLiteral
import app.revanced.patcher.patch.PatchException
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.patch.resourcePatch
import app.revanced.patcher.patch.stringOption
import app.revanced.patcher.util.Document
import app.revanced.patches.all.misc.resources.addResources
import app.revanced.patches.all.misc.resources.addResourcesPatch
import app.revanced.patches.shared.layout.branding.addBrandLicensePatch
import app.revanced.patches.shared.layout.branding.header.CUSTOM_HEADER_RESOURCE_NAME
import app.revanced.patches.shared.layout.branding.header.changeHeaderPatch
import app.revanced.patches.shared.layout.branding.header.variants
import app.revanced.patches.shared.misc.mapping.ResourceType
import app.revanced.patches.shared.misc.mapping.resourceMappingPatch
import app.revanced.patches.shared.misc.settings.preference.ListPreference
import app.revanced.patches.youtube.misc.settings.PreferenceScreen
import app.revanced.util.ResourceGroup
import app.revanced.util.Utils.trimIndentMultiline
import app.revanced.util.copyResources
import app.revanced.util.findElementByAttributeValueOrThrow
import app.revanced.util.forEachInstructionAsSequence
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import java.io.File
private val variants = arrayOf("light", "dark")
internal val variants = arrayOf("light", "dark")
private val logoResourceNames = arrayOf(
"revanced_header_minimal",
"revanced_header_rounded",
)
private val targetResourceDirectoryNames = mapOf(
"drawable-hdpi" to "194x72 px",
@ -32,25 +31,6 @@ private val targetResourceDirectoryNames = mapOf(
"drawable-xxxhdpi" to "512x192 px",
)
/**
* Header logos built into this patch.
*/
private val logoResourceNames = arrayOf(
"revanced_header_minimal",
"revanced_header_rounded",
)
/**
* Custom header resource/file name.
*/
private const val CUSTOM_HEADER_RESOURCE_NAME = "revanced_header_custom"
/**
* Custom header resource/file names.
*/
private val customHeaderResourceFileNames = variants.map { variant ->
"${CUSTOM_HEADER_RESOURCE_NAME}_$variant.png"
}.toTypedArray()
private const val EXTENSION_CLASS_DESCRIPTOR =
"Lapp/revanced/extension/youtube/patches/ChangeHeaderPatch;"
@ -97,182 +77,61 @@ private val changeHeaderBytecodePatch = bytecodePatch {
}
}
@Suppress("unused")
val changeHeaderPatch = resourcePatch(
name = "Change header",
description = "Adds an option to change the header logo in the top left corner of the app.",
) {
dependsOn(addResourcesPatch, changeHeaderBytecodePatch)
compatibleWith(
"com.google.android.youtube"(
val changeHeaderPatch = changeHeaderPatch(
targetResourceDirectoryNames = targetResourceDirectoryNames,
changeHeaderBytecodePatch = changeHeaderBytecodePatch,
compatiblePackages = arrayOf(
"com.google.android.youtube" to setOf(
"20.14.43",
"20.21.37",
"20.26.46",
"20.31.42",
"20.37.48",
"20.40.45"
),
)
val custom by stringOption(
name = "Custom header logo",
description = """
Folder with images to use as a custom header logo.
The folder must contain one or more of the following folders, depending on the DPI of the device:
${targetResourceDirectoryNames.keys.joinToString("\n") { "- $it" }}
Each of the folders must contain all of the following files:
${customHeaderResourceFileNames.joinToString("\n")}
The image dimensions must be as follows:
${targetResourceDirectoryNames.map { (dpi, dim) -> "- $dpi: $dim" }.joinToString("\n")}
""".trimIndentMultiline(),
)
apply {
addResources("youtube", "layout.branding.changeHeaderPatch")
PreferenceScreen.GENERAL.addPreferences(
if (custom == null) {
ListPreference("revanced_header_logo")
} else {
ListPreference(
key = "revanced_header_logo",
entriesKey = "revanced_header_logo_custom_entries",
entryValuesKey = "revanced_header_logo_custom_entry_values",
)
},
)
),
variants = variants,
logoResourceNames = logoResourceNames,
preferenceScreen = PreferenceScreen.GENERAL,
resourcesAppId = "youtube",
) {
// Logo is replaced using an attribute reference.
document("res/values/attrs.xml").use { document ->
val resources = document.childNodes.item(0)
logoResourceNames.forEach { logo ->
variants.forEach { variant ->
copyResources(
"change-header",
ResourceGroup(
"drawable",
logo + "_" + variant + ".xml",
),
)
}
fun addAttributeReference(logoName: String) {
val item = document.createElement("attr")
item.setAttribute("format", "reference")
item.setAttribute("name", logoName)
resources.appendChild(item)
}
// Copy custom template. Images are only used if settings
// are imported and a custom header is enabled.
targetResourceDirectoryNames.keys.forEach { dpi ->
variants.forEach { variant ->
copyResources(
"change-header",
ResourceGroup(
dpi,
*customHeaderResourceFileNames,
),
)
}
}
logoResourceNames.forEach { logoName -> addAttributeReference(logoName) }
// Logo is replaced using an attribute reference.
document("res/values/attrs.xml").use { document ->
val resources = document.childNodes.item(0)
addAttributeReference(CUSTOM_HEADER_RESOURCE_NAME)
}
fun addAttributeReference(logoName: String) {
val item = document.createElement("attr")
item.setAttribute("format", "reference")
// Add custom drawables to all styles that use the regular and premium logo.
document("res/values/styles.xml").use { document ->
arrayOf(
"Base.Theme.YouTube.Light" to "light",
"Base.Theme.YouTube.Dark" to "dark",
"CairoLightThemeRingo2Updates" to "light",
"CairoDarkThemeRingo2Updates" to "dark",
).forEach { (style, mode) ->
val styleElement = document.childNodes.findElementByAttributeValueOrThrow("name", style)
fun addDrawableElement(document: Document, logoName: String, mode: String) {
val item = document.createElement("item")
item.setAttribute("name", logoName)
resources.appendChild(item)
item.textContent = "@drawable/${logoName}_$mode"
styleElement.appendChild(item)
}
logoResourceNames.forEach { logoName ->
addAttributeReference(logoName)
}
logoResourceNames.forEach { logoName -> addDrawableElement(document, logoName, mode) }
addAttributeReference(CUSTOM_HEADER_RESOURCE_NAME)
}
// Add custom drawables to all styles that use the regular and premium logo.
document("res/values/styles.xml").use { document ->
arrayOf(
"Base.Theme.YouTube.Light" to "light",
"Base.Theme.YouTube.Dark" to "dark",
"CairoLightThemeRingo2Updates" to "light",
"CairoDarkThemeRingo2Updates" to "dark",
).forEach { (style, mode) ->
val styleElement = document.childNodes.findElementByAttributeValueOrThrow(
"name",
style,
)
fun addDrawableElement(document: Document, logoName: String, mode: String) {
val item = document.createElement("item")
item.setAttribute("name", logoName)
item.textContent = "@drawable/${logoName}_$mode"
styleElement.appendChild(item)
}
logoResourceNames.forEach { logoName ->
addDrawableElement(document, logoName, mode)
}
addDrawableElement(document, CUSTOM_HEADER_RESOURCE_NAME, mode)
}
}
// Copy user provided images last, so if an exception is thrown due to bad input.
if (custom != null) {
val customFile = File(custom!!.trim())
if (!customFile.exists()) {
throw PatchException(
"The custom header path cannot be found: " +
customFile.absolutePath,
)
}
if (!customFile.isDirectory) {
throw PatchException(
"The custom header path must be a folder: " +
customFile.absolutePath,
)
}
var copiedFiles = false
// For each source folder, copy the files to the target resource directories.
customFile.listFiles { file ->
file.isDirectory && file.name in targetResourceDirectoryNames
}!!.forEach { dpiSourceFolder ->
val targetDpiFolder = get("res").resolve(dpiSourceFolder.name)
if (!targetDpiFolder.exists()) {
// Should never happen.
throw IllegalStateException("Resource not found: $dpiSourceFolder")
}
val customFiles = dpiSourceFolder.listFiles { file ->
file.isFile && file.name in customHeaderResourceFileNames
}!!
if (customFiles.isNotEmpty() && customFiles.size != variants.size) {
throw PatchException(
"Both light/dark mode images " +
"must be specified but only found: " + customFiles.map { it.name },
)
}
customFiles.forEach { imgSourceFile ->
val imgTargetFile = targetDpiFolder.resolve(imgSourceFile.name)
imgSourceFile.copyTo(target = imgTargetFile, overwrite = true)
copiedFiles = true
}
}
if (!copiedFiles) {
throw PatchException(
"Expected to find directories and files: " +
customHeaderResourceFileNames.contentToString() +
"\nBut none were found in the provided option file path: " + customFile.absolutePath,
)
}
addDrawableElement(document, CUSTOM_HEADER_RESOURCE_NAME, mode)
}
}
}
}

View file

@ -188,6 +188,26 @@
</patch>
</app>
<app id="music">
<patch id="layout.branding.header.changeHeaderPatch">
<string-array name="revanced_header_logo_entries">
<item>@string/revanced_header_logo_entry_1</item>
<item>@string/revanced_header_logo_entry_2</item>
</string-array>
<string-array name="revanced_header_logo_entry_values">
<item>DEFAULT</item>
<item>REGULAR</item>
</string-array>
<string-array name="revanced_header_logo_custom_entries">
<item>@string/revanced_header_logo_entry_1</item>
<item>@string/revanced_header_logo_entry_2</item>
<item>@string/revanced_header_logo_entry_3</item>
</string-array>
<string-array name="revanced_header_logo_custom_entry_values">
<item>DEFAULT</item>
<item>REGULAR</item>
<item>CUSTOM</item>
</string-array>
</patch>
<patch id="layout.branding.customBrandingPatch">
<string-array name="revanced_custom_branding_name_entries">
<!-- Original must be first. -->
@ -427,7 +447,7 @@
<item>MODERN_3</item>
</string-array>
</patch>
<patch id="layout.branding.changeHeaderPatch">
<patch id="layout.branding.header.changeHeaderPatch">
<string-array name="revanced_header_logo_entries">
<item>@string/revanced_header_logo_entry_1</item>
<item>@string/revanced_header_logo_entry_2</item>

View file

@ -1568,7 +1568,7 @@ Swipe to expand or close"</string>
<string name="revanced_custom_branding_name_entry_3" translatable="false">YT ReVanced</string>
<string name="revanced_custom_branding_name_entry_4" translatable="false">YT</string>
</patch>
<patch id="layout.branding.changeHeaderPatch">
<patch id="layout.branding.header.changeHeaderPatch">
<string name="revanced_header_logo_title">Header logo</string>
<string name="revanced_header_logo_entry_1">Default</string>
<string name="revanced_header_logo_entry_2">Regular</string>
@ -1801,6 +1801,12 @@ Video playback with AV1 may stutter or drop frames."</string>
</patch>
</app>
<app id="music">
<patch id="layout.branding.header.changeHeaderPatch">
<string name="revanced_header_logo_title">Header logo</string>
<string name="revanced_header_logo_entry_1">Default</string>
<string name="revanced_header_logo_entry_2" translatable="false">ReVanced</string>
<string name="revanced_header_logo_entry_3">Custom</string>
</patch>
<patch id="layout.branding.customBrandingPatch">
<string name="revanced_custom_branding_name_entry_2" translatable="false">YT Music ReVanced</string>
<string name="revanced_custom_branding_name_entry_3" translatable="false">Music ReVanced</string>