fix(Spotify - Custom theme): Override more color resources (#4690)

This commit is contained in:
Nuckyz 2025-04-02 14:14:17 -03:00 committed by GitHub
parent 3c316fa329
commit d7a7a0b982
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 222 additions and 62 deletions

View File

@ -9,6 +9,7 @@ import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Color;
import android.net.ConnectivityManager;
import android.os.Build;
import android.os.Bundle;
@ -799,4 +800,14 @@ public class Utils {
builder.getContext().setTheme(editTextDialogStyle);
}
}
/**
* Parse a color resource or hex code to an int representation of the color.
*/
public static int getColorFromString(String colorString) throws IllegalArgumentException, Resources.NotFoundException {
if (colorString.startsWith("#")) {
return Color.parseColor(colorString);
}
return getResourceColor(colorString);
}
}

View File

@ -0,0 +1,22 @@
package app.revanced.extension.spotify.layout.theme;
import android.graphics.Color;
import app.revanced.extension.shared.Logger;
import app.revanced.extension.shared.Utils;
@SuppressWarnings("unused")
public final class CustomThemePatch {
/**
* Injection point.
*/
public static long getThemeColor(String colorString) {
try {
return Utils.getColorFromString(colorString);
} catch (Exception ex) {
Logger.printException(() -> "Invalid custom color: " + colorString, ex);
return Color.BLACK;
}
}
}

View File

@ -45,13 +45,24 @@ public class ThemeHelper {
return "@color/yt_black3";
}
private static int getThemeColor(String resourceName, int defaultColor) {
try {
return Utils.getColorFromString(resourceName);
} catch (Exception ex) {
// User entered an invalid custom theme color.
// Normally this should never be reached, and no localized strings are needed.
Utils.showToastLong("Invalid custom theme color: " + resourceName);
return defaultColor;
}
}
/**
* @return The dark theme color as specified by the Theme patch (if included),
* or the dark mode background color unpatched YT uses.
*/
public static int getDarkThemeColor() {
if (darkThemeColor == null) {
darkThemeColor = getColorInt(darkThemeResourceName());
darkThemeColor = getThemeColor(darkThemeResourceName(), Color.BLACK);
}
return darkThemeColor;
}
@ -71,18 +82,11 @@ public class ThemeHelper {
*/
public static int getLightThemeColor() {
if (lightThemeColor == null) {
lightThemeColor = getColorInt(lightThemeResourceName());
lightThemeColor = getThemeColor(lightThemeResourceName(), Color.WHITE);
}
return lightThemeColor;
}
private static int getColorInt(String colorString) {
if (colorString.startsWith("#")) {
return Color.parseColor(colorString);
}
return Utils.getResourceColor(colorString);
}
public static int getBackgroundColor() {
return isDarkTheme() ? getDarkThemeColor() : getLightThemeColor();
}
@ -96,6 +100,6 @@ public class ThemeHelper {
? "yt_black3"
: "yt_white1";
return getColorInt(colorName);
return Utils.getColorFromString(colorName);
}
}

View File

@ -0,0 +1,75 @@
package app.revanced.patches.spotify.layout.theme
import app.revanced.patcher.extensions.InstructionExtensions.addInstructions
import app.revanced.patcher.extensions.InstructionExtensions.getInstruction
import app.revanced.patcher.fingerprint
import app.revanced.patcher.patch.bytecodePatch
import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod
import app.revanced.patches.spotify.misc.extension.sharedExtensionPatch
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.reference.FieldReference
private const val EXTENSION_CLASS_DESCRIPTOR = "Lapp/revanced/extension/spotify/layout/theme/CustomThemePatch;"
internal val customThemeByteCodePatch = bytecodePatch {
dependsOn(sharedExtensionPatch)
val backgroundColor by spotifyBackgroundColor
val backgroundColorSecondary by spotifyBackgroundColorSecondary
execute {
fun MutableMethod.addColorChangeInstructions(literal: Long, colorString: String) {
val index = indexOfFirstLiteralInstructionOrThrow(literal)
val register = getInstruction<OneRegisterInstruction>(index).registerA
addInstructions(
index + 1,
"""
const-string v$register, "$colorString"
invoke-static { v$register }, $EXTENSION_CLASS_DESCRIPTOR->getThemeColor(Ljava/lang/String;)J
move-result-wide v$register
"""
)
}
val encoreColorsClassName = with(encoreThemeFingerprint) {
// Find index of the first static get found after the string constant.
val encoreColorsFieldReferenceIndex = originalMethod.indexOfFirstInstructionOrThrow(
stringMatches!!.first().index,
Opcode.SGET_OBJECT
)
originalMethod.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)
}
}
encoreColorsConstructorFingerprint.method.apply {
// Playlist song list background color.
addColorChangeInstructions(PLAYLIST_BACKGROUND_COLOR_LITERAL, backgroundColor!!)
// Share menu background color.
addColorChangeInstructions(SHARE_MENU_BACKGROUND_COLOR_LITERAL, backgroundColorSecondary!!)
}
homeCategoryPillColorsFingerprint.method.apply {
// Home category pills background color.
addColorChangeInstructions(HOME_CATEGORY_PILL_COLOR_LITERAL, backgroundColorSecondary!!)
}
settingsHeaderColorFingerprint.method.apply {
// Settings header background color.
addColorChangeInstructions(SETTINGS_HEADER_COLOR_LITERAL, backgroundColorSecondary!!)
}
}
}

View File

@ -1,59 +1,24 @@
@file:Suppress("NAME_SHADOWING")
package app.revanced.patches.spotify.layout.theme
import app.revanced.patcher.patch.resourcePatch
import app.revanced.patcher.patch.stringOption
import org.w3c.dom.Element
@Suppress("unused")
val customThemePatch = resourcePatch(
name = "Custom theme",
description = "Applies a custom theme.",
description = "Applies a custom theme (defaults to amoled black)",
use = false,
) {
compatibleWith("com.spotify.music")
val backgroundColor by 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,
)
dependsOn(customThemeByteCodePatch)
val backgroundColorSecondary by stringOption(
key = "backgroundColorSecondary",
default = "#ff282828",
title = "Secondary background color",
description = "The secondary background color. (e.g. search box, artist & podcast). 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 = "#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,
)
val backgroundColor by spotifyBackgroundColor()
val backgroundColorSecondary by spotifyBackgroundColorSecondary()
val accentColor by spotifyAccentColor()
val accentColorPressed by spotifyAccentColorPressed()
execute {
val backgroundColor = backgroundColor!!
val backgroundColorSecondary = backgroundColorSecondary!!
val accentColor = accentColor!!
val accentColorPressed = accentColorPressed!!
document("res/values/colors.xml").use { document ->
val resourcesNode = document.getElementsByTagName("resources").item(0) as Element
@ -61,20 +26,37 @@ val customThemePatch = resourcePatch(
for (i in 0 until childNodes.length) {
val node = childNodes.item(i) as? Element ?: continue
node.textContent =
when (node.getAttribute("name")) {
"dark_base_background_elevated_base", "design_dark_default_color_background",
"design_dark_default_color_surface", "gray_7", "gray_background", "gray_layer",
"sthlm_blk",
node.textContent = when (node.getAttribute("name")) {
// Gradient next to user photo and "All" in home page
"dark_base_background_base",
// Main background
"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",
// Song/player background
"bg_gradient_start_color", "bg_gradient_end_color",
// Login screen
"sthlm_blk", "sthlm_blk_grad_start", "stockholm_black",
// Misc
"image_placeholder_color",
-> backgroundColor
"gray_15" -> backgroundColorSecondary
// Track credits, merch in song player
"track_credits_card_bg", "benefit_list_default_color", "merch_card_background",
// Playlist list background in home page
"opacity_white_10",
// About artist background in song player
"gray_15",
// What's New pills background
"dark_base_background_tinted_highlight"
-> backgroundColorSecondary
"dark_brightaccent_background_base", "dark_base_text_brightaccent", "green_light" -> accentColor
"dark_brightaccent_background_press" -> accentColorPressed
else -> continue
}
"dark_brightaccent_background_base", "dark_base_text_brightaccent", "green_light" -> accentColor
"dark_brightaccent_background_press" -> accentColorPressed
else -> continue
}
}
}
}

View File

@ -0,0 +1,30 @@
package app.revanced.patches.spotify.layout.theme
import app.revanced.patcher.fingerprint
import app.revanced.util.containsLiteralInstruction
import com.android.tools.smali.dexlib2.AccessFlags
internal val encoreThemeFingerprint = fingerprint {
strings("Encore theme was not provided.") // Partial string match.
}
internal const val SETTINGS_HEADER_COLOR_LITERAL = 0xFF282828
internal const val HOME_CATEGORY_PILL_COLOR_LITERAL = 0xFF333333
internal const val PLAYLIST_BACKGROUND_COLOR_LITERAL = 0xFF121212
internal const val SHARE_MENU_BACKGROUND_COLOR_LITERAL = 0xFF1F1F1F
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 {
accessFlags(AccessFlags.STATIC, AccessFlags.CONSTRUCTOR)
custom { method, _ ->
method.containsLiteralInstruction(SETTINGS_HEADER_COLOR_LITERAL) &&
method.containsLiteralInstruction(0)
}
}

View File

@ -0,0 +1,36 @@
package app.revanced.patches.spotify.layout.theme
import app.revanced.patcher.patch.stringOption
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 spotifyBackgroundColorSecondary = stringOption(
key = "backgroundColorSecondary",
default = "#FF121212",
title = "Secondary background color",
description = "The secondary background color. (e.g. playlist list, player arist, 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,
)