fix(Spotify - Custom theme): Apply accent color in more places (#5039)

Co-authored-by: Nuckyz <61953774+Nuckyz@users.noreply.github.com>
This commit is contained in:
Cilly Leang
2025-06-03 18:02:15 +10:00
committed by GitHub
parent e8aa9c31eb
commit 9357887b6f
3 changed files with 165 additions and 133 deletions

View File

@ -8,15 +8,54 @@ import app.revanced.extension.shared.Utils;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public final class CustomThemePatch { public final class CustomThemePatch {
private static final int BACKGROUND_COLOR = getColorFromString("@color/gray_7");
private static final int BACKGROUND_COLOR_SECONDARY = getColorFromString("@color/gray_15");
private static final int ACCENT_COLOR = getColorFromString("@color/spotify_green_157");
private static final int ACCENT_PRESSED_COLOR =
getColorFromString("@color/dark_brightaccent_background_press");
/** /**
* Injection point. * Returns an int representation of the color resource or hex code.
*/ */
public static long getThemeColor(String colorString) { private static int getColorFromString(String colorString) {
try { try {
return Utils.getColorFromString(colorString); return Utils.getColorFromString(colorString);
} catch (Exception ex) { } catch (Exception ex) {
Logger.printException(() -> "Invalid custom color: " + colorString, ex); Logger.printException(() -> "Invalid color string: " + colorString, ex);
return Color.BLACK; return Color.BLACK;
} }
} }
/**
* Injection point. Returns an int representation of the replaced color from the original color.
*/
public static int replaceColor(int originalColor) {
switch (originalColor) {
// Playlist background color.
case 0xFF121212:
return BACKGROUND_COLOR;
// Share menu background color.
case 0xFF1F1F1F:
// Home category pills background color.
case 0xFF333333:
// Settings header background color.
case 0xFF282828:
// Spotify Connect device list background color.
case 0xFF2A2A2A:
return BACKGROUND_COLOR_SECONDARY;
// Some Lottie animations have a color that's slightly off due to rounding errors.
case 0xFF1ED760: case 0xFF1ED75F:
// Intermediate color used in some animations, same rounding issue.
case 0xFF1DB954: case 0xFF1CB854:
return ACCENT_COLOR;
case 0xFF1ABC54:
return ACCENT_PRESSED_COLOR;
default:
return originalColor;
}
}
} }

View File

@ -2,65 +2,19 @@ package app.revanced.patches.spotify.layout.theme
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.fingerprint
import app.revanced.patcher.patch.booleanOption import app.revanced.patcher.patch.booleanOption
import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.patch.resourcePatch import app.revanced.patcher.patch.resourcePatch
import app.revanced.patcher.patch.stringOption import app.revanced.patcher.patch.stringOption
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch
import app.revanced.patches.spotify.shared.IS_SPOTIFY_LEGACY_APP_TARGET import app.revanced.patches.spotify.shared.IS_SPOTIFY_LEGACY_APP_TARGET
import app.revanced.util.* import app.revanced.util.*
import com.android.tools.smali.dexlib2.AccessFlags
import com.android.tools.smali.dexlib2.Opcode
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
import com.android.tools.smali.dexlib2.iface.reference.FieldReference import com.android.tools.smali.dexlib2.iface.reference.MethodReference
import org.w3c.dom.Element import org.w3c.dom.Element
private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/spotify/layout/theme/CustomThemePatch;" private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/spotify/layout/theme/CustomThemePatch;"
internal val spotifyBackgroundColor = stringOption(
key = "backgroundColor",
default = "@android:color/black",
title = "Primary background color",
description = "The background color. Can be a hex color or a resource reference.",
required = true,
)
internal val overridePlayerGradientColor = booleanOption(
key = "overridePlayerGradientColor",
default = false,
title = "Override player gradient color",
description = "Apply primary background color to the player gradient color, which changes dynamically with the song.",
required = false
)
internal val spotifyBackgroundColorSecondary = stringOption(
key = "backgroundColorSecondary",
default = "#FF121212",
title = "Secondary background color",
description =
"The secondary background color. (e.g. playlist list in home, player artist, song credits). Can be a hex color or a resource reference.",
required = true,
)
internal val spotifyAccentColor = stringOption(
key = "accentColor",
default = "#FF1ED760",
title = "Accent color",
description = "The accent color ('Spotify green' by default). Can be a hex color or a resource reference.",
required = true,
)
internal val spotifyAccentColorPressed = stringOption(
key = "accentColorPressed",
default = "#FF169C46",
title = "Pressed dark theme accent color",
description =
"The color when accented buttons are pressed, by default slightly darker than accent. Can be a hex color or a resource reference.",
required = true,
)
private val customThemeBytecodePatch = bytecodePatch { private val customThemeBytecodePatch = bytecodePatch {
dependsOn(sharedExtensionPatch) dependsOn(sharedExtensionPatch)
@ -71,60 +25,60 @@ private val customThemeBytecodePatch = bytecodePatch {
return@execute return@execute
} }
fun MutableMethod.addColorChangeInstructions(literal: Long, colorString: String) { val colorSpaceUtilsClassDef = colorSpaceUtilsClassFingerprint.originalClassDef
val index = indexOfFirstLiteralInstructionOrThrow(literal)
val register = getInstruction<OneRegisterInstruction>(index).registerA // Hook a util method that converts ARGB to RGBA in the sRGB color space to replace hardcoded accent colors.
convertArgbToRgbaFingerprint.match(colorSpaceUtilsClassDef).method.apply {
addInstructions(
0,
"""
long-to-int p0, p0
invoke-static { p0 }, $EXTENSION_CLASS_DESCRIPTOR->replaceColor(I)I
move-result p0
int-to-long p0, p0
"""
)
}
// Lottie JSON parser method. It parses the JSON Lottie animation into its own class,
// including the solid color of it.
parseLottieJsonFingerprint.method.apply {
val invokeParseColorIndex = indexOfFirstInstructionOrThrow {
val reference = getReference<MethodReference>()
reference?.definingClass == "Landroid/graphics/Color;"
&& reference.name == "parseColor"
}
val parsedColorRegister = getInstruction<OneRegisterInstruction>(invokeParseColorIndex + 1).registerA
val replaceColorDescriptor = "$EXTENSION_CLASS_DESCRIPTOR->replaceColor(I)I"
addInstructions( addInstructions(
index + 1, invokeParseColorIndex + 2,
""" """
const-string v$register, "$colorString" # Use invoke-static/range because the register number is too large.
invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->getThemeColor(Ljava/lang/String;)J invoke-static/range { v$parsedColorRegister .. v$parsedColorRegister }, $replaceColorDescriptor
move-result-wide v$register move-result v$parsedColorRegister
""" """
) )
} }
val encoreColorsClassName = with(encoreThemeFingerprint.originalMethod) { // Lottie animated color parser.
// "Encore" colors are referenced right before the value of POSITIVE_INFINITY is returned. parseAnimatedColorFingerprint.method.apply {
// Begin the instruction find using the index of where POSITIVE_INFINITY is set into the register. val invokeArgbIndex = indexOfFirstInstructionOrThrow {
val positiveInfinityIndex = indexOfFirstLiteralInstructionOrThrow( val reference = getReference<MethodReference>()
Float.POSITIVE_INFINITY reference?.definingClass == "Landroid/graphics/Color;"
) && reference.name == "argb"
val encoreColorsFieldReferenceIndex = indexOfFirstInstructionReversedOrThrow(
positiveInfinityIndex,
Opcode.SGET_OBJECT
)
getInstruction(encoreColorsFieldReferenceIndex)
.getReference<FieldReference>()!!.definingClass
}
val encoreColorsConstructorFingerprint = fingerprint {
accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR)
custom { method, classDef ->
classDef.type == encoreColorsClassName &&
method.containsLiteralInstruction(PLAYLIST_BACKGROUND_COLOR_LITERAL)
} }
val argbColorRegister = getInstruction<OneRegisterInstruction>(invokeArgbIndex + 1).registerA
addInstructions(
invokeArgbIndex + 2,
"""
invoke-static { v$argbColorRegister }, $EXTENSION_CLASS_DESCRIPTOR->replaceColor(I)I
move-result v$argbColorRegister
"""
)
} }
val backgroundColor by spotifyBackgroundColor
val backgroundColorSecondary by spotifyBackgroundColorSecondary
encoreColorsConstructorFingerprint.method.apply {
addColorChangeInstructions(PLAYLIST_BACKGROUND_COLOR_LITERAL, backgroundColor!!)
addColorChangeInstructions(SHARE_MENU_BACKGROUND_COLOR_LITERAL, backgroundColorSecondary!!)
}
homeCategoryPillColorsFingerprint.method.addColorChangeInstructions(
HOME_CATEGORY_PILL_COLOR_LITERAL,
backgroundColorSecondary!!
)
settingsHeaderColorFingerprint.method.addColorChangeInstructions(
SETTINGS_HEADER_COLOR_LITERAL,
backgroundColorSecondary!!
)
} }
} }
@ -138,11 +92,48 @@ val customThemePatch = resourcePatch(
dependsOn(customThemeBytecodePatch) dependsOn(customThemeBytecodePatch)
val backgroundColor by spotifyBackgroundColor() val backgroundColor by stringOption(
val overridePlayerGradientColor by overridePlayerGradientColor() key = "backgroundColor",
val backgroundColorSecondary by spotifyBackgroundColorSecondary() default = "@android:color/black",
val accentColor by spotifyAccentColor() title = "Primary background color",
val accentColorPressed by spotifyAccentColorPressed() description = "The background color. Can be a hex color or a resource reference.",
required = true,
)
val overridePlayerGradientColor by booleanOption(
key = "overridePlayerGradientColor",
default = false,
title = "Override player gradient color",
description =
"Apply primary background color to the player gradient color, which changes dynamically with the song.",
required = false,
)
val backgroundColorSecondary by stringOption(
key = "backgroundColorSecondary",
default = "#FF121212",
title = "Secondary background color",
description = "The secondary background color. (e.g. playlist list in home, player artist, song credits). " +
"Can be a hex color or a resource reference.\",",
required = true,
)
val accentColor by stringOption(
key = "accentColor",
default = "#FF1ED760",
title = "Accent color",
description = "The accent color ('Spotify green' by default). Can be a hex color or a resource reference.",
required = true,
)
val accentColorPressed by stringOption(
key = "accentColorPressed",
default = "#FF1ABC54",
title = "Pressed dark theme accent color",
description = "The color when accented buttons are pressed, by default slightly darker than accent. " +
"Can be a hex color or a resource reference.",
required = true,
)
execute { execute {
document("res/values/colors.xml").use { document -> document("res/values/colors.xml").use { document ->
@ -161,34 +152,41 @@ val customThemePatch = resourcePatch(
} }
node.textContent = when (name) { node.textContent = when (name) {
// Main background color.
"gray_7",
// Left sidebar background color in tablet mode.
"gray_10",
// Gradient next to user photo and "All" in home page. // Gradient next to user photo and "All" in home page.
"dark_base_background_base", "dark_base_background_base",
// Main background. // "Add account", "Settings and privacy", "View Profile" left sidebar background color.
"gray_7",
// Left sidebar background in tablet mode.
"gray_10",
// "Add account", "Settings and privacy", "View Profile" left sidebar background.
"dark_base_background_elevated_base", "dark_base_background_elevated_base",
// Song/player gradient start/end color. // Song/player gradient start/end color.
"bg_gradient_start_color", "bg_gradient_end_color", "bg_gradient_start_color", "bg_gradient_end_color",
// Login screen background and gradient start. // Login screen background color and gradient start.
"sthlm_blk", "sthlm_blk_grad_start", "sthlm_blk", "sthlm_blk_grad_start",
// Misc. // Misc.
"image_placeholder_color", "image_placeholder_color",
-> backgroundColor -> backgroundColor
// Track credits, merch background in song player. // "About the artist" background color in song player.
"gray_15",
// Track credits, merch background color in song player.
"track_credits_card_bg", "benefit_list_default_color", "merch_card_background", "track_credits_card_bg", "benefit_list_default_color", "merch_card_background",
// Playlist list background in home page. // Playlist list background in home page.
"opacity_white_10", "opacity_white_10",
// "About the artist" background in song player.
"gray_15",
// "What's New" pills background. // "What's New" pills background.
"dark_base_background_tinted_highlight" "dark_base_background_tinted_highlight"
-> backgroundColorSecondary -> backgroundColorSecondary
"dark_brightaccent_background_base", "dark_base_text_brightaccent", "green_light" -> accentColor "dark_brightaccent_background_base",
"dark_brightaccent_background_press" -> accentColorPressed "dark_base_text_brightaccent",
"green_light",
"spotify_green_157"
-> accentColor
"dark_brightaccent_background_press"
-> accentColorPressed
else -> continue else -> continue
} }
} }
@ -198,8 +196,8 @@ val customThemePatch = resourcePatch(
document("res/drawable/start_screen_gradient.xml").use { document -> document("res/drawable/start_screen_gradient.xml").use { document ->
val gradientNode = document.getElementsByTagName("gradient").item(0) as Element val gradientNode = document.getElementsByTagName("gradient").item(0) as Element
gradientNode.setAttribute("android:startColor", backgroundColor) gradientNode.setAttribute("android:startColor", "@color/gray_7")
gradientNode.setAttribute("android:endColor", backgroundColor) gradientNode.setAttribute("android:endColor", "@color/gray_7")
} }
} }
} }

View File

@ -4,30 +4,25 @@ import app.revanced.patcher.fingerprint
import app.revanced.util.containsLiteralInstruction import app.revanced.util.containsLiteralInstruction
import com.android.tools.smali.dexlib2.AccessFlags import com.android.tools.smali.dexlib2.AccessFlags
internal val encoreThemeFingerprint = fingerprint { internal val colorSpaceUtilsClassFingerprint = fingerprint {
strings("Encore theme was not provided.") // Partial string match. strings("The specified color must be encoded in an RGB color space.") // Partial string match.
custom { method, _ ->
method.name == "invoke"
}
} }
internal const val PLAYLIST_BACKGROUND_COLOR_LITERAL = 0xFF121212 internal val convertArgbToRgbaFingerprint = fingerprint {
internal const val SHARE_MENU_BACKGROUND_COLOR_LITERAL = 0xFF1F1F1F accessFlags(AccessFlags.PUBLIC, AccessFlags.STATIC, AccessFlags.FINAL)
internal const val HOME_CATEGORY_PILL_COLOR_LITERAL = 0xFF333333 returns("J")
internal const val SETTINGS_HEADER_COLOR_LITERAL = 0xFF282828 parameters("J")
internal val homeCategoryPillColorsFingerprint = fingerprint{
accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR)
custom { method, _ ->
method.containsLiteralInstruction(HOME_CATEGORY_PILL_COLOR_LITERAL) &&
method.containsLiteralInstruction(0x33000000)
}
} }
internal val settingsHeaderColorFingerprint = fingerprint { internal val parseLottieJsonFingerprint = fingerprint {
accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR) strings("Unsupported matte type: ")
}
internal val parseAnimatedColorFingerprint = fingerprint {
parameters("L", "F")
returns("Ljava/lang/Object;")
custom { method, _ -> custom { method, _ ->
method.containsLiteralInstruction(SETTINGS_HEADER_COLOR_LITERAL) && method.containsLiteralInstruction(255.0) &&
method.containsLiteralInstruction(0) method.containsLiteralInstruction(1.0)
} }
} }