Skip to content

Commit d7a7a0b

Browse files
authored
fix(Spotify - Custom theme): Override more color resources (#4690)
1 parent 3c316fa commit d7a7a0b

File tree

7 files changed

+223
-63
lines changed

7 files changed

+223
-63
lines changed

extensions/shared/library/src/main/java/app/revanced/extension/shared/Utils.java

+11
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import android.content.pm.PackageManager;
1010
import android.content.res.Configuration;
1111
import android.content.res.Resources;
12+
import android.graphics.Color;
1213
import android.net.ConnectivityManager;
1314
import android.os.Build;
1415
import android.os.Bundle;
@@ -799,4 +800,14 @@ public static void setEditTextDialogTheme(AlertDialog.Builder builder) {
799800
builder.getContext().setTheme(editTextDialogStyle);
800801
}
801802
}
803+
804+
/**
805+
* Parse a color resource or hex code to an int representation of the color.
806+
*/
807+
public static int getColorFromString(String colorString) throws IllegalArgumentException, Resources.NotFoundException {
808+
if (colorString.startsWith("#")) {
809+
return Color.parseColor(colorString);
810+
}
811+
return getResourceColor(colorString);
812+
}
802813
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package app.revanced.extension.spotify.layout.theme;
2+
3+
import android.graphics.Color;
4+
5+
import app.revanced.extension.shared.Logger;
6+
import app.revanced.extension.shared.Utils;
7+
8+
@SuppressWarnings("unused")
9+
public final class CustomThemePatch {
10+
11+
/**
12+
* Injection point.
13+
*/
14+
public static long getThemeColor(String colorString) {
15+
try {
16+
return Utils.getColorFromString(colorString);
17+
} catch (Exception ex) {
18+
Logger.printException(() -> "Invalid custom color: " + colorString, ex);
19+
return Color.BLACK;
20+
}
21+
}
22+
}

extensions/youtube/src/main/java/app/revanced/extension/youtube/ThemeHelper.java

+14-10
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,24 @@ private static String darkThemeResourceName() {
4545
return "@color/yt_black3";
4646
}
4747

48+
private static int getThemeColor(String resourceName, int defaultColor) {
49+
try {
50+
return Utils.getColorFromString(resourceName);
51+
} catch (Exception ex) {
52+
// User entered an invalid custom theme color.
53+
// Normally this should never be reached, and no localized strings are needed.
54+
Utils.showToastLong("Invalid custom theme color: " + resourceName);
55+
return defaultColor;
56+
}
57+
}
58+
4859
/**
4960
* @return The dark theme color as specified by the Theme patch (if included),
5061
* or the dark mode background color unpatched YT uses.
5162
*/
5263
public static int getDarkThemeColor() {
5364
if (darkThemeColor == null) {
54-
darkThemeColor = getColorInt(darkThemeResourceName());
65+
darkThemeColor = getThemeColor(darkThemeResourceName(), Color.BLACK);
5566
}
5667
return darkThemeColor;
5768
}
@@ -71,18 +82,11 @@ private static String lightThemeResourceName() {
7182
*/
7283
public static int getLightThemeColor() {
7384
if (lightThemeColor == null) {
74-
lightThemeColor = getColorInt(lightThemeResourceName());
85+
lightThemeColor = getThemeColor(lightThemeResourceName(), Color.WHITE);
7586
}
7687
return lightThemeColor;
7788
}
7889

79-
private static int getColorInt(String colorString) {
80-
if (colorString.startsWith("#")) {
81-
return Color.parseColor(colorString);
82-
}
83-
return Utils.getResourceColor(colorString);
84-
}
85-
8690
public static int getBackgroundColor() {
8791
return isDarkTheme() ? getDarkThemeColor() : getLightThemeColor();
8892
}
@@ -96,6 +100,6 @@ public static int getToolbarBackgroundColor() {
96100
? "yt_black3"
97101
: "yt_white1";
98102

99-
return getColorInt(colorName);
103+
return Utils.getColorFromString(colorName);
100104
}
101105
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package app.revanced.patches.spotify.layout.theme
2+
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.bytecodePatch
7+
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
8+
import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch
9+
import app.revanced.util.*
10+
import com.android.tools.smali.dexlib2.AccessFlags
11+
import com.android.tools.smali.dexlib2.Opcode
12+
import com.android.tools.smali.dexlib2.iface.instruction.OneRegisterInstruction
13+
import com.android.tools.smali.dexlib2.iface.reference.FieldReference
14+
15+
private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/spotify/layout/theme/CustomThemePatch;"
16+
17+
internal val customThemeByteCodePatch = bytecodePatch {
18+
dependsOn(sharedExtensionPatch)
19+
20+
val backgroundColor by spotifyBackgroundColor
21+
val backgroundColorSecondary by spotifyBackgroundColorSecondary
22+
23+
execute {
24+
fun MutableMethod.addColorChangeInstructions(literal: Long, colorString: String) {
25+
val index = indexOfFirstLiteralInstructionOrThrow(literal)
26+
val register = getInstruction<OneRegisterInstruction>(index).registerA
27+
28+
addInstructions(
29+
index + 1,
30+
"""
31+
const-string v$register, "$colorString"
32+
invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->getThemeColor(Ljava/lang/String;)J
33+
move-result-wide v$register
34+
"""
35+
)
36+
}
37+
38+
val encoreColorsClassName = with(encoreThemeFingerprint) {
39+
// Find index of the first static get found after the string constant.
40+
val encoreColorsFieldReferenceIndex = originalMethod.indexOfFirstInstructionOrThrow(
41+
stringMatches!!.first().index,
42+
Opcode.SGET_OBJECT
43+
)
44+
45+
originalMethod.getInstruction(encoreColorsFieldReferenceIndex)
46+
.getReference<FieldReference>()!!.definingClass
47+
}
48+
49+
val encoreColorsConstructorFingerprint = fingerprint {
50+
accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR)
51+
custom { method, classDef ->
52+
classDef.type == encoreColorsClassName &&
53+
method.containsLiteralInstruction(PLAYLIST_BACKGROUND_COLOR_LITERAL)
54+
}
55+
}
56+
57+
encoreColorsConstructorFingerprint.method.apply {
58+
// Playlist song list background color.
59+
addColorChangeInstructions(PLAYLIST_BACKGROUND_COLOR_LITERAL, backgroundColor!!)
60+
61+
// Share menu background color.
62+
addColorChangeInstructions(SHARE_MENU_BACKGROUND_COLOR_LITERAL, backgroundColorSecondary!!)
63+
}
64+
65+
homeCategoryPillColorsFingerprint.method.apply {
66+
// Home category pills background color.
67+
addColorChangeInstructions(HOME_CATEGORY_PILL_COLOR_LITERAL, backgroundColorSecondary!!)
68+
}
69+
70+
settingsHeaderColorFingerprint.method.apply {
71+
// Settings header background color.
72+
addColorChangeInstructions(SETTINGS_HEADER_COLOR_LITERAL, backgroundColorSecondary!!)
73+
}
74+
}
75+
}

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

+35-53
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,62 @@
1-
@file:Suppress("NAME_SHADOWING")
2-
31
package app.revanced.patches.spotify.layout.theme
42

53
import app.revanced.patcher.patch.resourcePatch
6-
import app.revanced.patcher.patch.stringOption
74
import org.w3c.dom.Element
85

96
@Suppress("unused")
107
val customThemePatch = resourcePatch(
118
name = "Custom theme",
12-
description = "Applies a custom theme.",
9+
description = "Applies a custom theme (defaults to amoled black)",
1310
use = false,
1411
) {
1512
compatibleWith("com.spotify.music")
1613

17-
val backgroundColor by stringOption(
18-
key = "backgroundColor",
19-
default = "@android:color/black",
20-
title = "Primary background color",
21-
description = "The background color. Can be a hex color or a resource reference.",
22-
required = true,
23-
)
24-
25-
val backgroundColorSecondary by stringOption(
26-
key = "backgroundColorSecondary",
27-
default = "#ff282828",
28-
title = "Secondary background color",
29-
description = "The secondary background color. (e.g. search box, artist & podcast). Can be a hex color or a resource reference.",
30-
required = true,
31-
)
32-
33-
val accentColor by stringOption(
34-
key = "accentColor",
35-
default = "#ff1ed760",
36-
title = "Accent color",
37-
description = "The accent color ('Spotify green' by default). Can be a hex color or a resource reference.",
38-
required = true,
39-
)
14+
dependsOn(customThemeByteCodePatch)
4015

41-
val accentColorPressed by stringOption(
42-
key = "accentColorPressed",
43-
default = "#ff169c46",
44-
title = "Pressed dark theme accent color",
45-
description =
46-
"The color when accented buttons are pressed, by default slightly darker than accent. " +
47-
"Can be a hex color or a resource reference.",
48-
required = true,
49-
)
16+
val backgroundColor by spotifyBackgroundColor()
17+
val backgroundColorSecondary by spotifyBackgroundColorSecondary()
18+
val accentColor by spotifyAccentColor()
19+
val accentColorPressed by spotifyAccentColorPressed()
5020

5121
execute {
52-
val backgroundColor = backgroundColor!!
53-
val backgroundColorSecondary = backgroundColorSecondary!!
54-
val accentColor = accentColor!!
55-
val accentColorPressed = accentColorPressed!!
56-
5722
document("res/values/colors.xml").use { document ->
5823
val resourcesNode = document.getElementsByTagName("resources").item(0) as Element
5924

6025
val childNodes = resourcesNode.childNodes
6126
for (i in 0 until childNodes.length) {
6227
val node = childNodes.item(i) as? Element ?: continue
6328

64-
node.textContent =
65-
when (node.getAttribute("name")) {
66-
"dark_base_background_elevated_base", "design_dark_default_color_background",
67-
"design_dark_default_color_surface", "gray_7", "gray_background", "gray_layer",
68-
"sthlm_blk",
29+
node.textContent = when (node.getAttribute("name")) {
30+
// Gradient next to user photo and "All" in home page
31+
"dark_base_background_base",
32+
// Main background
33+
"gray_7",
34+
// Left sidebar background in tablet mode
35+
"gray_10",
36+
// Add account, Settings and privacy, View Profile left sidebar background
37+
"dark_base_background_elevated_base",
38+
// Song/player background
39+
"bg_gradient_start_color", "bg_gradient_end_color",
40+
// Login screen
41+
"sthlm_blk", "sthlm_blk_grad_start", "stockholm_black",
42+
// Misc
43+
"image_placeholder_color",
6944
-> backgroundColor
7045

71-
"gray_15" -> backgroundColorSecondary
72-
73-
"dark_brightaccent_background_base", "dark_base_text_brightaccent", "green_light" -> accentColor
74-
75-
"dark_brightaccent_background_press" -> accentColorPressed
76-
else -> continue
77-
}
46+
// Track credits, merch in song player
47+
"track_credits_card_bg", "benefit_list_default_color", "merch_card_background",
48+
// Playlist list background in home page
49+
"opacity_white_10",
50+
// About artist background in song player
51+
"gray_15",
52+
// What's New pills background
53+
"dark_base_background_tinted_highlight"
54+
-> backgroundColorSecondary
55+
56+
"dark_brightaccent_background_base", "dark_base_text_brightaccent", "green_light" -> accentColor
57+
"dark_brightaccent_background_press" -> accentColorPressed
58+
else -> continue
59+
}
7860
}
7961
}
8062
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package app.revanced.patches.spotify.layout.theme
2+
3+
import app.revanced.patcher.fingerprint
4+
import app.revanced.util.containsLiteralInstruction
5+
import com.android.tools.smali.dexlib2.AccessFlags
6+
7+
internal val encoreThemeFingerprint = fingerprint {
8+
strings("Encore theme was not provided.") // Partial string match.
9+
}
10+
11+
internal const val SETTINGS_HEADER_COLOR_LITERAL = 0xFF282828
12+
internal const val HOME_CATEGORY_PILL_COLOR_LITERAL = 0xFF333333
13+
internal const val PLAYLIST_BACKGROUND_COLOR_LITERAL = 0xFF121212
14+
internal const val SHARE_MENU_BACKGROUND_COLOR_LITERAL = 0xFF1F1F1F
15+
16+
internal val homeCategoryPillColorsFingerprint = fingerprint{
17+
accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR)
18+
custom { method, _ ->
19+
method.containsLiteralInstruction(HOME_CATEGORY_PILL_COLOR_LITERAL) &&
20+
method.containsLiteralInstruction(0x33000000)
21+
}
22+
}
23+
24+
internal val settingsHeaderColorFingerprint = fingerprint {
25+
accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR)
26+
custom { method, _ ->
27+
method.containsLiteralInstruction(SETTINGS_HEADER_COLOR_LITERAL) &&
28+
method.containsLiteralInstruction(0)
29+
}
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package app.revanced.patches.spotify.layout.theme
2+
3+
import app.revanced.patcher.patch.stringOption
4+
5+
internal val spotifyBackgroundColor = stringOption(
6+
key = "backgroundColor",
7+
default = "@android:color/black",
8+
title = "Primary background color",
9+
description = "The background color. Can be a hex color or a resource reference.",
10+
required = true,
11+
)
12+
13+
internal val spotifyBackgroundColorSecondary = stringOption(
14+
key = "backgroundColorSecondary",
15+
default = "#FF121212",
16+
title = "Secondary background color",
17+
description = "The secondary background color. (e.g. playlist list, player arist, credits). Can be a hex color or a resource reference.",
18+
required = true,
19+
)
20+
21+
internal val spotifyAccentColor = stringOption(
22+
key = "accentColor",
23+
default = "#FF1ED760",
24+
title = "Accent color",
25+
description = "The accent color ('Spotify green' by default). Can be a hex color or a resource reference.",
26+
required = true,
27+
)
28+
29+
internal val spotifyAccentColorPressed = stringOption(
30+
key = "accentColorPressed",
31+
default = "#FF169C46",
32+
title = "Pressed dark theme accent color",
33+
description =
34+
"The color when accented buttons are pressed, by default slightly darker than accent. Can be a hex color or a resource reference.",
35+
required = true,
36+
)

0 commit comments

Comments
 (0)