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:
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,61 +25,61 @@ 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 argbColorRegister = getInstruction<OneRegisterInstruction>(invokeArgbIndex + 1).registerA
|
||||||
|
|
||||||
val encoreColorsConstructorFingerprint = fingerprint {
|
addInstructions(
|
||||||
accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR)
|
invokeArgbIndex + 2,
|
||||||
custom { method, classDef ->
|
"""
|
||||||
classDef.type == encoreColorsClassName &&
|
invoke-static { v$argbColorRegister }, $EXTENSION_CLASS_DESCRIPTOR->replaceColor(I)I
|
||||||
method.containsLiteralInstruction(PLAYLIST_BACKGROUND_COLOR_LITERAL)
|
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!!
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user