Skip to content

Commit 0ee3693

Browse files
feat(Spotify - Custom theme): Add option to use unmodified player background gradient (#4741)
Co-authored-by: LisoUseInAIKyrios <[email protected]>
1 parent b99c2e5 commit 0ee3693

File tree

8 files changed

+398
-233
lines changed

8 files changed

+398
-233
lines changed

patches/api/patches.api

+12
Original file line numberDiff line numberDiff line change
@@ -1525,7 +1525,11 @@ public final class app/revanced/patches/yuka/misc/unlockpremium/UnlockPremiumPat
15251525
}
15261526

15271527
public final class app/revanced/util/BytecodeUtilsKt {
1528+
public static final fun addInstructionsAtControlFlowLabel (Lapp/revanced/patcher/util/proxy/mutableTypes/MutableMethod;ILjava/lang/String;)V
1529+
public static final fun containsLiteralInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;D)Z
1530+
public static final fun containsLiteralInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;F)Z
15281531
public static final fun containsLiteralInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;J)Z
1532+
public static final fun findFreeRegister (Lcom/android/tools/smali/dexlib2/iface/Method;I[I)I
15291533
public static final fun findInstructionIndicesReversed (Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/Opcode;)Ljava/util/List;
15301534
public static final fun findInstructionIndicesReversed (Lcom/android/tools/smali/dexlib2/iface/Method;Lkotlin/jvm/functions/Function1;)Ljava/util/List;
15311535
public static final fun findInstructionIndicesReversedOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;Lcom/android/tools/smali/dexlib2/Opcode;)Ljava/util/List;
@@ -1552,9 +1556,17 @@ public final class app/revanced/util/BytecodeUtilsKt {
15521556
public static final fun indexOfFirstInstructionReversedOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/Integer;Lkotlin/jvm/functions/Function1;)I
15531557
public static synthetic fun indexOfFirstInstructionReversedOrThrow$default (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/Integer;Lcom/android/tools/smali/dexlib2/Opcode;ILjava/lang/Object;)I
15541558
public static synthetic fun indexOfFirstInstructionReversedOrThrow$default (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/Integer;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)I
1559+
public static final fun indexOfFirstLiteralInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;D)I
1560+
public static final fun indexOfFirstLiteralInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;F)I
15551561
public static final fun indexOfFirstLiteralInstruction (Lcom/android/tools/smali/dexlib2/iface/Method;J)I
1562+
public static final fun indexOfFirstLiteralInstructionOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;D)I
1563+
public static final fun indexOfFirstLiteralInstructionOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;F)I
15561564
public static final fun indexOfFirstLiteralInstructionOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;J)I
1565+
public static final fun indexOfFirstLiteralInstructionReversed (Lcom/android/tools/smali/dexlib2/iface/Method;D)I
1566+
public static final fun indexOfFirstLiteralInstructionReversed (Lcom/android/tools/smali/dexlib2/iface/Method;F)I
15571567
public static final fun indexOfFirstLiteralInstructionReversed (Lcom/android/tools/smali/dexlib2/iface/Method;J)I
1568+
public static final fun indexOfFirstLiteralInstructionReversedOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;D)I
1569+
public static final fun indexOfFirstLiteralInstructionReversedOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;F)I
15581570
public static final fun indexOfFirstLiteralInstructionReversedOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;J)I
15591571
public static final fun indexOfFirstResourceId (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/String;)I
15601572
public static final fun indexOfFirstResourceIdOrThrow (Lcom/android/tools/smali/dexlib2/iface/Method;Ljava/lang/String;)I

patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/CustomThemeBytecodePatch.kt

-82
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,133 @@
11
package app.revanced.patches.spotify.layout.theme
22

3+
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
4+
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
5+
import app.revanced.patcher.fingerprint
6+
import app.revanced.patcher.patch.booleanOption
7+
import app.revanced.patcher.patch.bytecodePatch
38
import app.revanced.patcher.patch.resourcePatch
9+
import app.revanced.patcher.patch.stringOption
10+
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
11+
import app.revanced.patches.spotify.misc.extension.IS_SPOTIFY_LEGACY_APP_TARGET
12+
import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch
13+
import app.revanced.util.*
14+
import com.android.tools.smali.dexlib2.AccessFlags
15+
import com.android.tools.smali.dexlib2.Opcode
16+
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
17+
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
418
import org.w3c.dom.Element
519

20+
private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/spotify/layout/theme/CustomThemePatch;"
21+
22+
internal val spotifyBackgroundColor = stringOption(
23+
key = "backgroundColor",
24+
default = "@android:color/black",
25+
title = "Primary background color",
26+
description = "The background color. Can be a hex color or a resource reference.",
27+
required = true,
28+
)
29+
30+
internal val overridePlayerGradientColor = booleanOption(
31+
key = "overridePlayerGradientColor",
32+
default = false,
33+
title = "Override player gradient color",
34+
description = "Apply primary background color to the player gradient color, which changes dynamically with the song.",
35+
required = false
36+
)
37+
38+
internal val spotifyBackgroundColorSecondary = stringOption(
39+
key = "backgroundColorSecondary",
40+
default = "#FF121212",
41+
title = "Secondary background color",
42+
description =
43+
"The secondary background color. (e.g. playlist list in home, player artist, song credits). Can be a hex color or a resource reference.",
44+
required = true,
45+
)
46+
47+
internal val spotifyAccentColor = stringOption(
48+
key = "accentColor",
49+
default = "#FF1ED760",
50+
title = "Accent color",
51+
description = "The accent color ('Spotify green' by default). Can be a hex color or a resource reference.",
52+
required = true,
53+
)
54+
55+
internal val spotifyAccentColorPressed = stringOption(
56+
key = "accentColorPressed",
57+
default = "#FF169C46",
58+
title = "Pressed dark theme accent color",
59+
description =
60+
"The color when accented buttons are pressed, by default slightly darker than accent. Can be a hex color or a resource reference.",
61+
required = true,
62+
)
63+
64+
private val customThemeBytecodePatch = bytecodePatch {
65+
dependsOn(sharedExtensionPatch)
66+
67+
execute {
68+
if (IS_SPOTIFY_LEGACY_APP_TARGET) {
69+
// Bytecode changes are not needed for legacy app target.
70+
// Player background color is changed with existing resource patch.
71+
return@execute
72+
}
73+
74+
fun MutableMethod.addColorChangeInstructions(literal: Long, colorString: String) {
75+
val index = indexOfFirstLiteralInstructionOrThrow(literal)
76+
val register = getInstruction<OneRegisterInstruction>(index).registerA
77+
78+
addInstructions(
79+
index + 1,
80+
"""
81+
const-string v$register, "$colorString"
82+
invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->getThemeColor(Ljava/lang/String;)J
83+
move-result-wide v$register
84+
"""
85+
)
86+
}
87+
88+
val encoreColorsClassName = with(encoreThemeFingerprint.originalMethod) {
89+
// "Encore" colors are referenced right before the value of POSITIVE_INFINITY is returned.
90+
// Begin the instruction find using the index of where POSITIVE_INFINITY is set into the register.
91+
val positiveInfinityIndex = indexOfFirstLiteralInstructionOrThrow(
92+
Float.POSITIVE_INFINITY
93+
)
94+
val encoreColorsFieldReferenceIndex = indexOfFirstInstructionReversedOrThrow(
95+
positiveInfinityIndex,
96+
Opcode.SGET_OBJECT
97+
)
98+
99+
getInstruction(encoreColorsFieldReferenceIndex)
100+
.getReference<FieldReference>()!!.definingClass
101+
}
102+
103+
val encoreColorsConstructorFingerprint = fingerprint {
104+
accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR)
105+
custom { method, classDef ->
106+
classDef.type == encoreColorsClassName &&
107+
method.containsLiteralInstruction(PLAYLIST_BACKGROUND_COLOR_LITERAL)
108+
}
109+
}
110+
111+
val backgroundColor by spotifyBackgroundColor
112+
val backgroundColorSecondary by spotifyBackgroundColorSecondary
113+
114+
encoreColorsConstructorFingerprint.method.apply {
115+
addColorChangeInstructions(PLAYLIST_BACKGROUND_COLOR_LITERAL, backgroundColor!!)
116+
addColorChangeInstructions(SHARE_MENU_BACKGROUND_COLOR_LITERAL, backgroundColorSecondary!!)
117+
}
118+
119+
homeCategoryPillColorsFingerprint.method.addColorChangeInstructions(
120+
HOME_CATEGORY_PILL_COLOR_LITERAL,
121+
backgroundColorSecondary!!
122+
)
123+
124+
settingsHeaderColorFingerprint.method.addColorChangeInstructions(
125+
SETTINGS_HEADER_COLOR_LITERAL,
126+
backgroundColorSecondary!!
127+
)
128+
}
129+
}
130+
6131
@Suppress("unused")
7132
val customThemePatch = resourcePatch(
8133
name = "Custom theme",
@@ -11,9 +136,10 @@ val customThemePatch = resourcePatch(
11136
) {
12137
compatibleWith("com.spotify.music")
13138

14-
dependsOn(customThemeByteCodePatch)
139+
dependsOn(customThemeBytecodePatch)
15140

16141
val backgroundColor by spotifyBackgroundColor()
142+
val overridePlayerGradientColor by overridePlayerGradientColor()
17143
val backgroundColorSecondary by spotifyBackgroundColorSecondary()
18144
val accentColor by spotifyAccentColor()
19145
val accentColorPressed by spotifyAccentColorPressed()
@@ -25,31 +151,39 @@ val customThemePatch = resourcePatch(
25151
val childNodes = resourcesNode.childNodes
26152
for (i in 0 until childNodes.length) {
27153
val node = childNodes.item(i) as? Element ?: continue
154+
val name = node.getAttribute("name")
155+
156+
// Skip overriding song/player gradient start color if the option is disabled.
157+
// Gradient end color should be themed regardless to allow the gradient to connect with
158+
// our primary background color.
159+
if (name == "bg_gradient_start_color" && !overridePlayerGradientColor!!) {
160+
continue
161+
}
28162

29-
node.textContent = when (node.getAttribute("name")) {
30-
// Gradient next to user photo and "All" in home page
163+
node.textContent = when (name) {
164+
// Gradient next to user photo and "All" in home page.
31165
"dark_base_background_base",
32-
// Main background
166+
// Main background.
33167
"gray_7",
34-
// Left sidebar background in tablet mode
168+
// Left sidebar background in tablet mode.
35169
"gray_10",
36-
// Add account, Settings and privacy, View Profile left sidebar background
170+
// "Add account", "Settings and privacy", "View Profile" left sidebar background.
37171
"dark_base_background_elevated_base",
38-
// Song/player background
172+
// Song/player gradient start/end color.
39173
"bg_gradient_start_color", "bg_gradient_end_color",
40-
// Login screen
41-
"sthlm_blk", "sthlm_blk_grad_start", "stockholm_black",
42-
// Misc
174+
// Login screen background and gradient start.
175+
"sthlm_blk", "sthlm_blk_grad_start",
176+
// Misc.
43177
"image_placeholder_color",
44178
-> backgroundColor
45179

46-
// Track credits, merch in song player
180+
// Track credits, merch background in song player.
47181
"track_credits_card_bg", "benefit_list_default_color", "merch_card_background",
48-
// Playlist list background in home page
182+
// Playlist list background in home page.
49183
"opacity_white_10",
50-
// About artist background in song player
184+
// "About the artist" background in song player.
51185
"gray_15",
52-
// What's New pills background
186+
// "What's New" pills background.
53187
"dark_base_background_tinted_highlight"
54188
-> backgroundColorSecondary
55189

@@ -59,5 +193,13 @@ val customThemePatch = resourcePatch(
59193
}
60194
}
61195
}
196+
197+
// Login screen gradient.
198+
document("res/drawable/start_screen_gradient.xml").use { document ->
199+
val gradientNode = document.getElementsByTagName("gradient").item(0) as Element
200+
201+
gradientNode.setAttribute("android:startColor", backgroundColor)
202+
gradientNode.setAttribute("android:endColor", backgroundColor)
203+
}
62204
}
63205
}

patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/Fingerprints.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import app.revanced.util.containsLiteralInstruction
55
import com.android.tools.smali.dexlib2.AccessFlags
66

77
internal val encoreThemeFingerprint = fingerprint {
8-
strings("Encore theme was not provided.") // Partial string match.
8+
strings("No EncoreLayoutTheme provided")
99
}
1010

11-
internal const val SETTINGS_HEADER_COLOR_LITERAL = 0xFF282828
12-
internal const val HOME_CATEGORY_PILL_COLOR_LITERAL = 0xFF333333
1311
internal const val PLAYLIST_BACKGROUND_COLOR_LITERAL = 0xFF121212
1412
internal const val SHARE_MENU_BACKGROUND_COLOR_LITERAL = 0xFF1F1F1F
13+
internal const val HOME_CATEGORY_PILL_COLOR_LITERAL = 0xFF333333
14+
internal const val SETTINGS_HEADER_COLOR_LITERAL = 0xFF282828
1515

1616
internal val homeCategoryPillColorsFingerprint = fingerprint{
1717
accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR)

patches/src/main/kotlin/app/revanced/patches/spotify/layout/theme/Options.kt

-36
This file was deleted.

0 commit comments

Comments
 (0)