diff --git a/extensions/music/src/main/java/app/revanced/extension/music/patches/ChangeHeaderPatch.java b/extensions/music/src/main/java/app/revanced/extension/music/patches/ChangeHeaderPatch.java new file mode 100644 index 0000000000..143bedd549 --- /dev/null +++ b/extensions/music/src/main/java/app/revanced/extension/music/patches/ChangeHeaderPatch.java @@ -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; + } + } +} diff --git a/extensions/music/src/main/java/app/revanced/extension/music/settings/Settings.java b/extensions/music/src/main/java/app/revanced/extension/music/settings/Settings.java index b2f61541a4..4ce48cbbbf 100644 --- a/extensions/music/src/main/java/app/revanced/extension/music/settings/Settings.java +++ b/extensions/music/src/main/java/app/revanced/extension/music/settings/Settings.java @@ -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 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); diff --git a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/YouTubeAndMusicSettings.java b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/YouTubeAndMusicSettings.java index 67adab4efa..8a0b7c8e18 100644 --- a/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/YouTubeAndMusicSettings.java +++ b/extensions/shared/library/src/main/java/app/revanced/extension/shared/settings/YouTubeAndMusicSettings.java @@ -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)); + } diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/branding/CustomBrandingPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/branding/CustomBrandingPatch.kt index ff6b5326a5..d1b17e2e3f 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/layout/branding/CustomBrandingPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/branding/CustomBrandingPatch.kt @@ -65,7 +65,6 @@ val customBrandingPatch = baseCustomBrandingPatch( mainActivityName = MUSIC_MAIN_ACTIVITY_NAME, activityAliasNameWithIntents = MUSIC_MAIN_ACTIVITY_NAME, preferenceScreen = PreferenceScreen.GENERAL, - block = { dependsOn(sharedExtensionPatch, disableSplashAnimationPatch) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/layout/branding/header/ChangeHeaderPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/layout/branding/header/ChangeHeaderPatch.kt new file mode 100644 index 0000000000..d6c7886266 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/layout/branding/header/ChangeHeaderPatch.kt @@ -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(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", +) diff --git a/patches/src/main/kotlin/app/revanced/patches/shared/layout/branding/header/ChangeHeaderPatch.kt b/patches/src/main/kotlin/app/revanced/patches/shared/layout/branding/header/ChangeHeaderPatch.kt new file mode 100644 index 0000000000..d03d18feb6 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/shared/layout/branding/header/ChangeHeaderPatch.kt @@ -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, + changeHeaderBytecodePatch: Patch, + vararg compatiblePackages: Package, + variants: Array, + logoResourceNames: Array, + 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, + ) + } + } + } + } +} \ No newline at end of file diff --git a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/header/ChangeHeaderPatch.kt b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/header/ChangeHeaderPatch.kt index 4555a64b3c..b7f1e6ad25 100644 --- a/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/header/ChangeHeaderPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/youtube/layout/branding/header/ChangeHeaderPatch.kt @@ -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) } } -} +} \ No newline at end of file diff --git a/patches/src/main/resources/addresources/values/arrays.xml b/patches/src/main/resources/addresources/values/arrays.xml index 230b9b8f38..a29199d359 100644 --- a/patches/src/main/resources/addresources/values/arrays.xml +++ b/patches/src/main/resources/addresources/values/arrays.xml @@ -188,6 +188,26 @@ + + + @string/revanced_header_logo_entry_1 + @string/revanced_header_logo_entry_2 + + + DEFAULT + REGULAR + + + @string/revanced_header_logo_entry_1 + @string/revanced_header_logo_entry_2 + @string/revanced_header_logo_entry_3 + + + DEFAULT + REGULAR + CUSTOM + + @@ -427,7 +447,7 @@ MODERN_3 - + @string/revanced_header_logo_entry_1 @string/revanced_header_logo_entry_2 diff --git a/patches/src/main/resources/addresources/values/strings.xml b/patches/src/main/resources/addresources/values/strings.xml index 95da75bad4..27cec1b91f 100644 --- a/patches/src/main/resources/addresources/values/strings.xml +++ b/patches/src/main/resources/addresources/values/strings.xml @@ -1568,7 +1568,7 @@ Swipe to expand or close" YT ReVanced YT - + Header logo Default Regular @@ -1801,6 +1801,12 @@ Video playback with AV1 may stutter or drop frames." + + Header logo + Default + ReVanced + Custom + YT Music ReVanced Music ReVanced