diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/patcher/steps/patch/SmaliPatchStep.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/patcher/steps/patch/SmaliPatchStep.kt
index 8e4361e..e97e626 100644
--- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/patcher/steps/patch/SmaliPatchStep.kt
+++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/patcher/steps/patch/SmaliPatchStep.kt
@@ -95,7 +95,19 @@ class SmaliPatchStep(
val entry = zip.openEntry(patchFile)
?: throw FileNotFoundException("Missing zip entry: $patchFile")
out.canonicalFile.parentFile?.mkdirs()
- out.writeBytes(entry.read())
+ var smaliText = entry.read()
+ .decodeToString()
+ .replace("\r\n", "\n")
+ for ((token, value) in substitutions) {
+ if (smaliText.contains(token)) {
+ smaliText = smaliText.replace(token, value)
+ container.log("Applied substitution $token -> $value in $patchFile")
+ }
+ }
+ UNRESOLVED_TOKEN.find(smaliText)?.let { match ->
+ throw Error("Unresolved option placeholder ${match.value} in $patchFile")
+ }
+ out.writeText(smaliText)
container.log("Extracted extension smali: $relative")
continue
}
diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt
index fce43e7..5bb3193 100644
--- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt
+++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt
@@ -155,6 +155,12 @@ enum class KnownPatch(
defaultIndex = 0,
token = "RL_WAZE_ROOT_ID",
),
+ PatchOption.Color(
+ key = "accent_color",
+ titleRes = R.string.patch_waze_accent_color_title,
+ default = -16747037, // Waze default #ff0075e3
+ token = "RL_WAZE_THEME_COLOR",
+ ),
),
),
LyricsProgressPill(
diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOption.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOption.kt
index 78d895b..9fcabf2 100644
--- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOption.kt
+++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOption.kt
@@ -65,6 +65,14 @@ sealed interface PatchOption {
val requiresOption: String? = null,
val token: String? = null,
) : PatchOption
+
+ data class Color(
+ override val key: String,
+ @StringRes override val titleRes: Int,
+ @StringRes override val descRes: Int = 0,
+ val default: Int,
+ val token: String? = null,
+ ) : PatchOption
}
data class ChoiceEntry(
diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptions.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptions.kt
index 96a1292..243a9d0 100644
--- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptions.kt
+++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptions.kt
@@ -56,6 +56,9 @@ data class PatchOptions(
(optionInts["${spec.id}/${option.key}"] ?: option.defaultIndex)
.coerceIn(0, option.entries.lastIndex.coerceAtLeast(0))
+ fun colorValue(spec: PatchSpec, option: OptionSpec.Color): Int =
+ optionInts["${spec.id}/${option.key}"] ?: option.default
+
fun isToggleOn(spec: PatchSpec, option: OptionSpec.Toggle): Boolean =
optionBools["${spec.id}/${option.key}"] ?: option.default
@@ -135,6 +138,10 @@ data class PatchOptions(
val index = if (gatedOff) 0 else choiceIndex(spec, option)
put("__${token}__", option.values.getOrNull(index) ?: continue)
}
+ is OptionSpec.Color -> {
+ val token = option.token ?: continue
+ put("__${token}__", colorValue(spec, option).toString())
+ }
}
}
}
diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsModel.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsModel.kt
index 3f6462e..8065d4e 100644
--- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsModel.kt
+++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsModel.kt
@@ -153,11 +153,19 @@ class PatchOptionsModel(
optionInts = optionInts + (keyOf(spec, option) to index)
}
+ fun colorValue(spec: PatchSpec, option: OptionSpec.Color): Int =
+ optionInts[keyOf(spec, option)] ?: option.default
+
+ fun setColorValue(spec: PatchSpec, option: OptionSpec.Color, value: Int) {
+ optionInts = optionInts + (keyOf(spec, option) to value)
+ }
+
fun isAdvancedModified(spec: PatchSpec): Boolean = spec.advancedOptions.any { option ->
when (option) {
is OptionSpec.Toggle -> toggleValue(spec, option) != option.default
is OptionSpec.Slider -> sliderValue(spec, option) != option.default
is OptionSpec.Choice -> choiceValue(spec, option) != option.defaultIndex
+ is OptionSpec.Color -> colorValue(spec, option) != option.default
}
}
@@ -175,6 +183,8 @@ class PatchOptionsModel(
setSlider = ::setSliderValue,
choice = ::choiceValue,
setChoice = ::setChoiceValue,
+ color = ::colorValue,
+ setColor = ::setColorValue,
isModified = ::isAdvancedModified,
reset = ::resetAdvanced,
)
diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchSpec.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchSpec.kt
index c0bca4c..12bb65e 100644
--- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchSpec.kt
+++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchSpec.kt
@@ -106,6 +106,18 @@ sealed interface OptionSpec {
val requiresOption: String? = null,
val token: String? = null,
) : OptionSpec
+
+ @Immutable
+ @Serializable
+ @SerialName("color")
+ data class Color(
+ override val key: String,
+ override val title: String = "",
+ override val description: String = "",
+ val default: Int = 0,
+ /** Placeholder name (without the surrounding `__`) baked into the `.patch`/extension files. */
+ val token: String? = null,
+ ) : OptionSpec
}
@Serializable
@@ -194,6 +206,14 @@ private fun PatchOption.toSpec(resolve: (Int) -> String): OptionSpec = when (thi
requiresOption = requiresOption,
token = token,
)
+
+ is PatchOption.Color -> OptionSpec.Color(
+ key = key,
+ title = resolve(titleRes),
+ description = if (descRes != 0) resolve(descRes) else "",
+ default = default,
+ token = token,
+ )
}
@Immutable
@@ -258,6 +278,8 @@ class PatchOptionState(
val setSlider: (PatchSpec, OptionSpec.Slider, Float) -> Unit,
val choice: (PatchSpec, OptionSpec.Choice) -> Int,
val setChoice: (PatchSpec, OptionSpec.Choice, Int) -> Unit,
+ val color: (PatchSpec, OptionSpec.Color) -> Int,
+ val setColor: (PatchSpec, OptionSpec.Color, Int) -> Unit,
val isModified: (PatchSpec) -> Boolean,
val reset: (PatchSpec) -> Unit,
) {
@@ -270,6 +292,8 @@ class PatchOptionState(
setSlider = { _, _, _ -> },
choice = { _, option -> option.defaultIndex },
setChoice = { _, _, _ -> },
+ color = { _, option -> option.default },
+ setColor = { _, _, _ -> },
isModified = { false },
reset = {},
)
diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchAdvancedOptionsSheet.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchAdvancedOptionsSheet.kt
index f21d4d5..20e1ee7 100644
--- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchAdvancedOptionsSheet.kt
+++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchAdvancedOptionsSheet.kt
@@ -7,6 +7,8 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.*
@@ -15,11 +17,15 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.fromHtml
+import androidx.compose.ui.text.input.KeyboardCapitalization
+import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@@ -196,6 +202,12 @@ fun PatchAdvancedOptionsSheet(
onSelect = { state.setChoice(patch, option, it) },
)
}
+
+ is OptionSpec.Color -> ColorOptionRow(
+ title = option.title,
+ color = state.color(patch, option),
+ onColorChange = { state.setColor(patch, option, it) },
+ )
}
}
@@ -433,6 +445,150 @@ private fun ChoiceOptionRow(
}
}
+@Composable
+private fun ColorOptionRow(
+ title: String,
+ color: Int,
+ onColorChange: (Int) -> Unit,
+) {
+ var showPicker by rememberSaveable { mutableStateOf(false) }
+ val selectedColor = color.withOpaqueAlpha()
+ val materialYouColor = MaterialTheme.colorScheme.primary.toArgb().withOpaqueAlpha()
+ val presets = listOf(
+ stringResource(R.string.patch_waze_color_waze_default),
+ stringResource(R.string.patch_waze_color_tidal_cyan),
+ stringResource(R.string.patch_waze_color_material_you),
+ )
+ val presetColors = listOf(WAZE_DEFAULT, TIDAL_CYAN, materialYouColor)
+ val selectedPreset = presetColors.indexOf(selectedColor)
+
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(8.dp),
+ ) {
+ Text(
+ text = title,
+ style = MaterialTheme.typography.labelMedium,
+ color = MaterialTheme.colorScheme.onSurfaceVariant,
+ modifier = Modifier.align(Alignment.CenterHorizontally),
+ )
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(10.dp),
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(MaterialTheme.shapes.medium)
+ .clickable { showPicker = true }
+ .padding(vertical = 4.dp),
+ ) {
+ ColorSwatch(color = selectedColor)
+ Text(
+ text = selectedColor.toRgbHex(),
+ style = MaterialTheme.typography.labelLarge,
+ color = MaterialTheme.colorScheme.primary,
+ )
+ }
+ SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) {
+ presets.forEachIndexed { index, label ->
+ SegmentedButton(
+ selected = index == selectedPreset,
+ onClick = { onColorChange(presetColors[index]) },
+ shape = SegmentedButtonDefaults.itemShape(index = index, count = presets.size),
+ icon = {},
+ label = {
+ Text(
+ text = label,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ textAlign = TextAlign.Center,
+ style = MaterialTheme.typography.labelMedium,
+ )
+ },
+ )
+ }
+ }
+ }
+
+ if (showPicker) {
+ ColorPickerDialog(
+ initialColor = selectedColor,
+ onDismiss = { showPicker = false },
+ onColorSelected = {
+ onColorChange(it)
+ showPicker = false
+ },
+ )
+ }
+}
+
+@Composable
+private fun ColorSwatch(color: Int, modifier: Modifier = Modifier) {
+ Box(
+ modifier = modifier
+ .size(32.dp)
+ .clip(CircleShape)
+ .background(Color(color))
+ .border(1.dp, MaterialTheme.colorScheme.outlineVariant, CircleShape),
+ )
+}
+
+@Composable
+private fun ColorPickerDialog(
+ initialColor: Int,
+ onDismiss: () -> Unit,
+ onColorSelected: (Int) -> Unit,
+) {
+ var color by rememberSaveable(initialColor) { mutableIntStateOf(initialColor.withOpaqueAlpha()) }
+ var hex by rememberSaveable(initialColor) { mutableStateOf(color.toRgbHex()) }
+ val parsedHex = parseRgbHex(hex)
+
+ AlertDialog(
+ onDismissRequest = onDismiss,
+ title = { Text(stringResource(R.string.patch_color_picker_title)) },
+ text = {
+ Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(12.dp),
+ ) {
+ ColorSwatch(color = parsedHex ?: color, modifier = Modifier.size(40.dp))
+ OutlinedTextField(
+ value = hex,
+ onValueChange = { value ->
+ hex = value
+ parseRgbHex(value)?.let { color = it }
+ },
+ label = { Text(stringResource(R.string.patch_color_picker_hex)) },
+ singleLine = true,
+ isError = parsedHex == null,
+ keyboardOptions = KeyboardOptions(
+ capitalization = KeyboardCapitalization.Characters,
+ keyboardType = KeyboardType.Ascii,
+ ),
+ supportingText = if (parsedHex == null) {
+ { Text(stringResource(R.string.patch_color_picker_hex_error)) }
+ } else null,
+ modifier = Modifier.weight(1f),
+ )
+ }
+ }
+ },
+ confirmButton = {
+ TextButton(
+ enabled = parsedHex != null,
+ onClick = { parsedHex?.let(onColorSelected) },
+ ) {
+ Text(stringResource(R.string.action_done))
+ }
+ },
+ dismissButton = {
+ TextButton(onClick = onDismiss) {
+ Text(stringResource(R.string.action_cancel))
+ }
+ },
+ )
+}
+
@Composable
private fun OptionText(
title: String,
@@ -465,6 +621,26 @@ private fun formatSliderValue(option: OptionSpec.Slider, value: Float): String {
}
}
+private const val OPAQUE_ALPHA = -0x1000000
+private const val WAZE_DEFAULT = -16747037 // #ff0075e3
+private const val TIDAL_CYAN = -14549268 // #ff21feec
+
+private fun Int.withOpaqueAlpha(): Int = (this and 0x00FFFFFF) or OPAQUE_ALPHA
+
+private fun Int.toRgbHex(): String = "#" +
+ (this and 0x00FFFFFF).toString(16).uppercase().padStart(6, '0')
+
+private fun parseRgbHex(value: String): Int? {
+ val clean = value.trim()
+ .removePrefix("#")
+ .removePrefix("0x")
+ .removePrefix("0X")
+ if (clean.length != 6 && clean.length != 8) return null
+ val parsed = clean.toLongOrNull(16) ?: return null
+ val rgb = if (clean.length == 8) parsed and 0x00FFFFFFL else parsed
+ return (0xFF000000L or rgb).toInt()
+}
+
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun OptionLockDialog(lock: OptionLock, onDismiss: () -> Unit) {
diff --git a/Manager/app/src/main/res/values/strings.xml b/Manager/app/src/main/res/values/strings.xml
index 3241b18..f1aa74a 100644
--- a/Manager/app/src/main/res/values/strings.xml
+++ b/Manager/app/src/main/res/values/strings.xml
@@ -289,9 +289,16 @@
Waze Integration
Control TIDAL playback from the Waze app
Media Menu
+ Accent Color
+ Waze Default
+ TIDAL Cyan
+ Material You
Auto Menu
Home
Recents
+ Custom color
+ Hex color
+ Use #RRGGBB or #AARRGGBB.
Enable Legacy UI
putInt(Ljava/lang/String;I)V
diff --git a/patches/manifest.json b/patches/manifest.json
index 4b81738..23bfda2 100644
--- a/patches/manifest.json
+++ b/patches/manifest.json
@@ -233,6 +233,13 @@
],
"defaultIndex": 0,
"token": "RL_WAZE_ROOT_ID"
+ },
+ {
+ "type": "color",
+ "key": "accent_color",
+ "title": "Accent Color",
+ "default": -16747037,
+ "token": "RL_WAZE_THEME_COLOR"
}
]
},