mirror of
https://github.com/meowarex/rl-mobile.git
synced 2026-08-27 14:37:45 +10:00
Integrations UI + Package Name Warnings/Gate <3
This commit is contained in:
+3
-1
@@ -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",
|
||||
|
||||
+27
-5
@@ -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()
|
||||
}
|
||||
|
||||
+53
-6
@@ -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) {
|
||||
|
||||
+12
-1
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+88
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+19
-3
@@ -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(
|
||||
|
||||
+35
-4
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user