mirror of
https://github.com/meowarex/rl-mobile.git
synced 2026-08-26 14:07:45 +10:00
feat(waze patch): customizable themeColor
This commit is contained in:
+13
-1
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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 = {},
|
||||
)
|
||||
|
||||
+176
@@ -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) {
|
||||
|
||||
@@ -289,9 +289,16 @@
|
||||
<string name="patch_waze_integration_title">Waze Integration</string>
|
||||
<string name="patch_waze_integration_desc">Control TIDAL playback from the Waze app</string>
|
||||
<string name="patch_waze_browse_root_title">Media Menu</string>
|
||||
<string name="patch_waze_accent_color_title">Accent Color</string>
|
||||
<string name="patch_waze_color_waze_default">Waze Default</string>
|
||||
<string name="patch_waze_color_tidal_cyan">TIDAL Cyan</string>
|
||||
<string name="patch_waze_color_material_you">Material You</string>
|
||||
<string name="patch_waze_root_auto">Auto Menu</string>
|
||||
<string name="patch_waze_root_home">Home</string>
|
||||
<string name="patch_waze_root_recents">Recents</string>
|
||||
<string name="patch_color_picker_title">Custom color</string>
|
||||
<string name="patch_color_picker_hex">Hex color</string>
|
||||
<string name="patch_color_picker_hex_error">Use #RRGGBB or #AARRGGBB.</string>
|
||||
<string name="patch_enable_legacy_ui_title">Enable Legacy UI</string>
|
||||
<string
|
||||
name="patch_enable_legacy_ui_desc"
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
|
||||
const-string v1, "themeColor"
|
||||
|
||||
const v2, -0xde0114 # TIDAL cyan #ff21feec
|
||||
const v2, __RL_WAZE_THEME_COLOR__ # Waze SDK themeColor
|
||||
|
||||
invoke-virtual {v0, v1, v2}, Landroid/os/Bundle;->putInt(Ljava/lang/String;I)V
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user