Integrations UI + Package Name Warnings/Gate <3

This commit is contained in:
2026-07-03 20:19:16 +10:00
parent 47c9c37236
commit 08aee155c9
11 changed files with 261 additions and 20 deletions
@@ -35,12 +35,14 @@ private fun PatchOptionsScreenPreview(
packageName = parameters.packageName,
packageNameState = parameters.packageNameState,
setPackageName = {},
packageNameLocked = parameters.packageName == PatchOptions.Default.packageName,
onUnlockPackageName = {},
onLockPackageName = {},
customTidalApk = parameters.customTidalApk,
onSelectCustomTidalApk = {},
customPatches = parameters.customPatches,
onSelectCustomPatches = {},
specs = specs,
enabledPatchCount = specs.size,
isPatchEnabled = { true },
onTogglePatch = { _, _ -> },
patchLockState = { PatchLock.Free },
@@ -24,6 +24,8 @@ enum class KnownPatch(
val variants: List<PatchVariant> = emptyList(),
val defaultVariantIndex: Int = 0,
val advancedOptions: List<PatchOption> = emptyList(),
val category: String = PatchSpec.CATEGORY_PATCH,
val pathLocked: Boolean = false,
) {
LyricsDisableCover(
order = 41,
@@ -140,6 +142,8 @@ enum class KnownPatch(
titleRes = R.string.patch_waze_integration_title,
descRes = R.string.patch_waze_integration_desc,
default = Disabled,
category = PatchSpec.CATEGORY_INTEGRATION,
pathLocked = true,
advancedOptions = listOf(
PatchOption.Choice(
key = "browse_root",
@@ -43,6 +43,24 @@ class PatchOptionsModel(
fetchPkgNameStateDebounced()
}
/** Patches flagged [PatchSpec.pathLocked] only work under the stock package name. */
fun isBlockedByPackageName(spec: PatchSpec): Boolean =
spec.pathLocked && packageName != PatchOptions.Default.packageName
/** The package-name field starts locked & editing requires confirming the unlock dialog */
var packageNameLocked by mutableStateOf(prefilledOptions.packageName == PatchOptions.Default.packageName)
private set
fun unlockPackageName() {
packageNameLocked = false
}
/** Re-locking snaps the package name back to the stock default */
fun lockPackageName() {
packageNameLocked = true
changePackageName(PatchOptions.Default.packageName)
}
var appName by mutableStateOf(prefilledOptions.appName)
private set
@@ -83,7 +101,7 @@ class PatchOptionsModel(
.coerceIn(0, spec.variants.lastIndex.coerceAtLeast(0))
fun isPatchEnabled(spec: PatchSpec): Boolean =
patchStates[spec.id] ?: spec.defaultEnabled
!isBlockedByPackageName(spec) && (patchStates[spec.id] ?: spec.defaultEnabled)
fun setPatchEnabled(spec: PatchSpec, enabled: Boolean) {
val byId = specs.associateBy { it.id }
@@ -190,6 +208,7 @@ class PatchOptionsModel(
)
fun lockState(spec: PatchSpec): PatchLock {
if (isBlockedByPackageName(spec)) return PatchLock.RequiresDefaultPackage
if (spec.variants.isNotEmpty()) return PatchLock.Free
val byId = specs.associateBy { it.id }
@@ -216,9 +235,6 @@ class PatchOptionsModel(
return PatchLock.Free
}
val enabledPatchCount: Int
get() = specs.count { isPatchEnabled(it) }
var customTidalApk by mutableStateOf<PatchComponent?>(null)
private set
var customPatches by mutableStateOf<PatchComponent?>(null)
@@ -333,7 +349,10 @@ class PatchOptionsModel(
debuggable = debuggable,
customTidalApk = customTidalApk,
customPatches = customPatches,
patchStates = patchStates,
// Hard-disable package-name-bound patches so the patcher can't apply
patchStates = patchStates.toMutableMap().apply {
specs.filter { isBlockedByPackageName(it) }.forEach { this[it.id] = false }
},
selectedVariants = selectedVariants,
optionFloats = optionFloats,
optionBools = optionBools,
@@ -395,4 +414,7 @@ sealed class PatchLock {
data object Free : PatchLock()
data class LockedOn(val by: PatchSpec) : PatchLock()
data class LockedOff(val by: PatchSpec) : PatchLock()
/** Locked off because the install uses a non-default package name */
data object RequiresDefaultPackage : PatchLock()
}
@@ -19,7 +19,9 @@ import com.meowarex.rlmobile.R
import com.meowarex.rlmobile.ui.components.*
import com.meowarex.rlmobile.ui.screens.componentopts.PatchComponent
import com.meowarex.rlmobile.ui.screens.patching.PatchingScreen
import androidx.compose.runtime.saveable.rememberSaveable
import com.meowarex.rlmobile.ui.screens.patchopts.components.PackageNameStateLabel
import com.meowarex.rlmobile.ui.screens.patchopts.components.PackageNameUnlockDialog
import com.meowarex.rlmobile.ui.screens.patchopts.components.PatchOptionsAppBar
import com.meowarex.rlmobile.ui.screens.patchopts.components.PatchSelectionAccordion
import com.meowarex.rlmobile.ui.screens.patchopts.components.options.*
@@ -55,13 +57,16 @@ class PatchOptionsScreen(
packageNameState = model.packageNameState,
setPackageName = model::changePackageName,
packageNameLocked = model.packageNameLocked,
onUnlockPackageName = model::unlockPackageName,
onLockPackageName = model::lockPackageName,
customTidalApk = model.customTidalApk,
customPatches = model.customPatches,
onSelectCustomTidalApk = { model.selectCustomTidalApk(navigator) },
onSelectCustomPatches = { model.selectCustomPatches(navigator) },
specs = model.specs,
enabledPatchCount = model.enabledPatchCount,
isPatchEnabled = model::isPatchEnabled,
onTogglePatch = model::setPatchEnabled,
patchLockState = model::lockState,
@@ -93,13 +98,16 @@ fun PatchOptionsScreenContent(
packageNameState: PackageNameState,
setPackageName: (String) -> Unit,
packageNameLocked: Boolean,
onUnlockPackageName: () -> Unit,
onLockPackageName: () -> Unit,
customTidalApk: PatchComponent?,
onSelectCustomTidalApk: () -> Unit,
customPatches: PatchComponent?,
onSelectCustomPatches: () -> Unit,
specs: List<PatchSpec>,
enabledPatchCount: Int,
isPatchEnabled: (PatchSpec) -> Boolean,
onTogglePatch: (PatchSpec, Boolean) -> Unit,
patchLockState: (PatchSpec) -> PatchLock,
@@ -110,6 +118,18 @@ fun PatchOptionsScreenContent(
isConfigValid: Boolean,
onInstall: () -> Unit,
) {
var showPkgUnlockDialog by rememberSaveable { mutableStateOf(false) }
if (showPkgUnlockDialog) {
PackageNameUnlockDialog(
blockedTitles = specs.filter { it.pathLocked }.map { it.title },
onConfirm = {
showPkgUnlockDialog = false
onUnlockPackageName()
},
onDismiss = { showPkgUnlockDialog = false },
)
}
Scaffold(
topBar = { PatchOptionsAppBar(isUpdate = isUpdate) },
) { paddingValues ->
@@ -159,6 +179,10 @@ fun PatchOptionsScreenContent(
valueIsDefault = packageNameIsDefault,
onValueChange = setPackageName,
onValueReset = { setPackageName(PatchOptions.Default.packageName) },
locked = packageNameLocked,
onLockClick = {
if (packageNameLocked) showPkgUnlockDialog = true else onLockPackageName()
},
) {
PackageNameStateLabel(
state = packageNameState,
@@ -167,17 +191,40 @@ fun PatchOptionsScreenContent(
}
}
val (integrations, patches) = remember(specs) { specs.partition { it.isIntegration } }
if (integrations.isNotEmpty()) {
PatchSelectionAccordion(
iconRes = R.drawable.ic_extension,
titleRes = R.string.patchopts_integrations_title,
descriptionRes = R.string.patchopts_integrations_desc,
specs = integrations,
enabledCount = integrations.count(isPatchEnabled),
totalCount = integrations.size,
isEnabled = isPatchEnabled,
onToggle = onTogglePatch,
lockState = patchLockState,
variantIndex = variantIndex,
onSelectVariant = onSelectVariant,
optionState = optionState,
modifier = Modifier.padding(top = 4.dp),
)
}
PatchSelectionAccordion(
specs = specs,
enabledCount = enabledPatchCount,
totalCount = specs.size,
iconRes = R.drawable.ic_healing,
titleRes = R.string.patchopts_patches_title,
descriptionRes = R.string.patchopts_patches_desc,
specs = patches,
enabledCount = patches.count(isPatchEnabled),
totalCount = patches.size,
isEnabled = isPatchEnabled,
onToggle = onTogglePatch,
lockState = patchLockState,
variantIndex = variantIndex,
onSelectVariant = onSelectVariant,
optionState = optionState,
modifier = Modifier.padding(top = 4.dp),
modifier = if (integrations.isEmpty()) Modifier.padding(top = 4.dp) else Modifier,
)
if (isDevMode) {
@@ -29,7 +29,16 @@ data class PatchSpec(
val variants: List<VariantSpec> = emptyList(),
val defaultVariantIndex: Int = 0,
val advancedOptions: List<OptionSpec> = emptyList(),
)
val category: String = CATEGORY_PATCH,
val pathLocked: Boolean = false,
) {
val isIntegration: Boolean get() = category == CATEGORY_INTEGRATION
companion object {
const val CATEGORY_PATCH = "patch"
const val CATEGORY_INTEGRATION = "integration"
}
}
@Immutable
@Serializable
@@ -163,6 +172,8 @@ fun builtinPatchSpecs(resolve: (Int) -> String): List<PatchSpec> =
variants = patch.variants.map { VariantSpec(resolve(it.titleRes), it.fileNames, it.extensionFiles) },
defaultVariantIndex = patch.defaultVariantIndex,
advancedOptions = patch.advancedOptions.map { it.toSpec(resolve) },
category = patch.category,
pathLocked = patch.pathLocked,
)
}
@@ -0,0 +1,88 @@
package com.meowarex.rlmobile.ui.screens.patchopts.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.fromHtml
import androidx.compose.ui.unit.dp
import com.meowarex.rlmobile.R
import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptions
/**
* Confirms unlocking the package-name field, warning that a custom name
* blocks the listed patches/integrations from working.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PackageNameUnlockDialog(
blockedTitles: List<String>,
onConfirm: () -> Unit,
onDismiss: () -> Unit,
) {
BasicAlertDialog(onDismissRequest = onDismiss) {
Surface(
shape = MaterialTheme.shapes.large,
color = MaterialTheme.colorScheme.surfaceContainerHigh,
tonalElevation = 6.dp,
) {
Column(
modifier = Modifier.padding(start = 24.dp, end = 12.dp, top = 20.dp, bottom = 8.dp),
verticalArrangement = Arrangement.spacedBy(6.dp),
) {
Text(
text = stringResource(R.string.patchopts_pkgname_warning_title),
style = MaterialTheme.typography.titleLarge,
)
Text(
text = AnnotatedString.fromHtml(
stringResource(R.string.patchopts_pkgname_warning_msg, PatchOptions.Default.packageName),
),
style = MaterialTheme.typography.bodyLarge,
)
if (blockedTitles.isNotEmpty()) {
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier.padding(start = 4.dp),
) {
for (title in blockedTitles) {
Text(
text = "$title",
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Bold,
)
}
}
}
Text(
text = stringResource(R.string.patchopts_pkgname_warning_permanent),
style = MaterialTheme.typography.bodyLarge,
)
Box(modifier = Modifier.fillMaxWidth()) {
Row(modifier = Modifier.align(Alignment.CenterEnd)) {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.action_cancel))
}
TextButton(onClick = onConfirm) {
Text(stringResource(R.string.action_continue))
}
}
}
}
}
}
}
@@ -1,6 +1,8 @@
package com.meowarex.rlmobile.ui.screens.patchopts.components
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
@@ -27,6 +29,7 @@ import com.meowarex.rlmobile.R
import com.meowarex.rlmobile.ui.screens.patchopts.OptionSpec
import com.meowarex.rlmobile.ui.screens.patchopts.PatchLock
import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptionState
import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptions
import com.meowarex.rlmobile.ui.screens.patchopts.PatchSpec
import com.meowarex.rlmobile.ui.screens.patchopts.effectiveVariants
import com.meowarex.rlmobile.ui.screens.patchopts.resolveVariantIndex
@@ -35,6 +38,9 @@ private data class LockInfo(val patch: PatchSpec, val lock: PatchLock)
@Composable
fun PatchSelectionAccordion(
@DrawableRes iconRes: Int,
@StringRes titleRes: Int,
@StringRes descriptionRes: Int,
specs: List<PatchSpec>,
enabledCount: Int,
totalCount: Int,
@@ -69,12 +75,17 @@ fun PatchSelectionAccordion(
.clickable(role = Role.Button) { expanded = !expanded }
.padding(horizontal = 16.dp, vertical = 14.dp),
) {
Icon(
painter = painterResource(iconRes),
contentDescription = null,
)
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier.weight(1f),
) {
Text(
text = stringResource(R.string.patchopts_patches_title),
text = stringResource(titleRes),
style = MaterialTheme.typography.titleMedium,
)
Text(
@@ -106,7 +117,7 @@ fun PatchSelectionAccordion(
.padding(horizontal = 16.dp, vertical = 12.dp),
) {
Text(
text = stringResource(R.string.patchopts_patches_desc),
text = stringResource(descriptionRes),
style = MaterialTheme.typography.bodySmall,
modifier = Modifier
.alpha(.7f)
@@ -372,6 +383,11 @@ private fun PatchLockDialog(
R.string.patch_lock_blocked_msg,
lock.by.title,
)
PatchLock.RequiresDefaultPackage -> Triple(
R.string.patch_lock_pkgname_title,
R.string.patch_lock_pkgname_msg,
PatchOptions.Default.packageName,
)
PatchLock.Free -> return
}
@@ -391,7 +407,7 @@ private fun PatchLockDialog(
)
Text(
text = AnnotatedString.fromHtml(stringResource(msgRes, blockerTitle)),
style = MaterialTheme.typography.titleMedium,
style = MaterialTheme.typography.bodyLarge,
)
Box(modifier = Modifier.fillMaxWidth()) {
TextButton(
@@ -3,8 +3,12 @@ package com.meowarex.rlmobile.ui.screens.patchopts.components.options
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.meowarex.rlmobile.R
import com.meowarex.rlmobile.ui.components.Label
import com.meowarex.rlmobile.ui.components.ResetToDefaultButton
@@ -18,6 +22,8 @@ fun TextPatchOption(
onValueChange: (String) -> Unit,
onValueReset: () -> Unit,
modifier: Modifier = Modifier,
locked: Boolean? = null,
onLockClick: () -> Unit = {},
extra: (@Composable ColumnScope.() -> Unit)? = null,
) {
Label(
@@ -30,16 +36,41 @@ fun TextPatchOption(
onValueChange = onValueChange,
isError = valueIsError,
singleLine = true,
enabled = locked != true,
colors = OutlinedTextFieldDefaults.colors(
unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainerLow,
focusedContainerColor = MaterialTheme.colorScheme.surfaceContainerLow,
errorContainerColor = MaterialTheme.colorScheme.surfaceContainerLow,
disabledContainerColor = MaterialTheme.colorScheme.surfaceContainerLow,
),
trailingIcon = {
ResetToDefaultButton(
enabled = !valueIsDefault,
onClick = onValueReset,
)
Row(verticalAlignment = Alignment.CenterVertically) {
if (locked != true) {
ResetToDefaultButton(
enabled = !valueIsDefault,
onClick = onValueReset,
)
}
if (locked != null) {
IconButton(
onClick = onLockClick,
modifier = Modifier.size(32.dp),
) {
Icon(
painter = painterResource(
if (locked) R.drawable.ic_lock else R.drawable.ic_lock_open,
),
contentDescription = stringResource(
if (locked) R.string.patchopts_field_unlock else R.string.patchopts_field_lock,
),
tint = if (locked) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(18.dp),
)
}
Spacer(Modifier.width(8.dp))
}
}
},
modifier = Modifier
.fillMaxWidth()