feat: patch options screen previews & improve layout

This commit is contained in:
rushiiMachine 2025-03-29 12:33:45 -07:00
parent f988846af2
commit 713d308f3e
No known key found for this signature in database
GPG key ID: DCBE5952BB3B6420
6 changed files with 252 additions and 111 deletions

View file

@ -0,0 +1,95 @@
package com.aliucord.manager.ui.previews.screens
import android.content.res.Configuration
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.*
import com.aliucord.manager.ui.components.ManagerTheme
import com.aliucord.manager.ui.screens.patchopts.*
import com.aliucord.manager.ui.util.DiscordVersion
// This preview has scrollable/interactable content that cannot be tested from an IDE preview
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun PatchOptionsScreenPreview(
@PreviewParameter(ContributorsProvider::class)
parameters: PatchOptionsParameters,
) {
ManagerTheme {
PatchOptionsScreenContent(
isUpdate = parameters.isUpdate,
isDevMode = parameters.isDevMode,
supportedVersion = parameters.supportedVersion,
debuggable = parameters.debuggable,
setDebuggable = {},
replaceIcon = parameters.replaceIcon,
setReplaceIcon = {},
appName = parameters.appName,
appNameIsError = parameters.appNameIsError,
setAppName = {},
packageName = parameters.packageName,
packageNameState = parameters.packageNameState,
setPackageName = {},
isConfigValid = parameters.isConfigValid,
onInstall = {},
)
}
}
private data class PatchOptionsParameters(
val isUpdate: Boolean,
val isDevMode: Boolean,
val supportedVersion: DiscordVersion,
val debuggable: Boolean,
val replaceIcon: Boolean,
val appName: String,
val appNameIsError: Boolean,
val packageName: String,
val packageNameState: PackageNameState,
val isConfigValid: Boolean,
)
private class ContributorsProvider : PreviewParameterProvider<PatchOptionsParameters> {
private val stableVersion = DiscordVersion.Existing(DiscordVersion.Type.STABLE, "126.21", 126021)
override val values = sequenceOf(
// Default initial install
PatchOptionsParameters(
isUpdate = false,
isDevMode = false,
supportedVersion = stableVersion,
debuggable = false,
replaceIcon = true,
appName = PatchOptions.Default.appName,
appNameIsError = false,
packageName = PatchOptions.Default.packageName,
packageNameState = PackageNameState.Ok,
isConfigValid = true,
),
PatchOptionsParameters(
isUpdate = true,
isDevMode = false,
supportedVersion = stableVersion,
debuggable = false,
replaceIcon = true,
appName = "an invalid app name.",
appNameIsError = true,
packageName = "a b",
packageNameState = PackageNameState.Invalid,
isConfigValid = false,
),
PatchOptionsParameters(
isUpdate = false,
isDevMode = true,
supportedVersion = stableVersion,
debuggable = true,
replaceIcon = false,
appName = PatchOptions.Default.appName,
appNameIsError = false,
packageName = PatchOptions.Default.packageName,
packageNameState = PackageNameState.Taken,
isConfigValid = true,
),
)
}

View file

@ -24,19 +24,11 @@ class PatchOptionsModel(
var packageNameState by mutableStateOf(PackageNameState.Ok)
private set
val packageNameIsDefault by derivedStateOf {
packageName == PatchOptions.Default.packageName
}
fun changePackageName(newPackageName: String) {
packageName = newPackageName
fetchPkgNameStateDebounced()
}
fun resetPackageName() {
changePackageName(PatchOptions.Default.packageName)
}
// ---------- App name state ----------
var appName by mutableStateOf(prefilledOptions.appName)
private set
@ -44,19 +36,11 @@ class PatchOptionsModel(
var appNameIsError by mutableStateOf(false)
private set
val appNameIsDefault by derivedStateOf {
appName == PatchOptions.Default.appName
}
fun changeAppName(newAppName: String) {
appName = newAppName
appNameIsError = newAppName.length !in (1..150)
}
fun resetAppName() {
appName = PatchOptions.Default.appName
}
// ---------- Icon patching state ----------
var replaceIcon by mutableStateOf(prefilledOptions.iconReplacement == PatchOptions.IconReplacement.Aliucord)
private set

View file

@ -6,25 +6,28 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.koin.getScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.aliucord.manager.R
import com.aliucord.manager.ui.components.AnimatedVersionDisplay
import com.aliucord.manager.ui.components.TextDivider
import com.aliucord.manager.ui.components.VersionDisplay
import com.aliucord.manager.ui.components.dialogs.NetworkWarningDialog
import com.aliucord.manager.ui.components.dialogs.UnknownSourcesPermissionDialog
import com.aliucord.manager.ui.screens.patching.PatchingScreen
import com.aliucord.manager.ui.screens.patchopts.components.PackageNameState
import com.aliucord.manager.ui.screens.patchopts.components.PackageNameStateLabel
import com.aliucord.manager.ui.screens.patchopts.components.PatchOptionsAppBar
import com.aliucord.manager.ui.screens.patchopts.components.options.SwitchPatchOption
import com.aliucord.manager.ui.screens.patchopts.components.options.TextPatchOption
@ -48,7 +51,7 @@ class PatchOptionsScreen(
override fun Content() {
val context = LocalContext.current
val navigator = LocalNavigator.currentOrThrow
val model = getScreenModel<PatchOptionsModel>() { parametersOf(prefilledOptions ?: PatchOptions.Default) }
val model = getScreenModel<PatchOptionsModel> { parametersOf(prefilledOptions ?: PatchOptions.Default) }
LaunchedEffect(Unit) {
// Ensure that when popping this screen off the stack that permission requests don't get triggered
@ -74,100 +77,160 @@ class PatchOptionsScreen(
UnknownSourcesPermissionDialog()
Scaffold(
topBar = { PatchOptionsAppBar(isUpdate = prefilledOptions != null) },
) { paddingValues ->
Column(
verticalArrangement = Arrangement.spacedByLastAtBottom(20.dp),
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(paddingValues)
.padding(horizontal = 20.dp, vertical = 10.dp)
) {
Text(
text = stringResource(R.string.patchopts_title),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
)
PatchOptionsScreenContent(
isUpdate = prefilledOptions != null,
isDevMode = model.isDevMode,
supportedVersion = supportedVersion,
var animatedVersion by rememberSaveable { mutableStateOf<DiscordVersion>(DiscordVersion.None) }
LaunchedEffect(Unit) {
animatedVersion = supportedVersion
debuggable = model.debuggable,
setDebuggable = model::changeDebuggable,
replaceIcon = model.replaceIcon,
setReplaceIcon = model::changeReplaceIcon,
appName = model.appName,
appNameIsError = model.appNameIsError,
setAppName = model::changeAppName,
packageName = model.packageName,
packageNameState = model.packageNameState,
setPackageName = model::changePackageName,
isConfigValid = model.isConfigValid,
onInstall = { navigator.push(PatchingScreen(model.generateConfig())) },
)
}
}
@Composable
fun PatchOptionsScreenContent(
isUpdate: Boolean,
isDevMode: Boolean,
supportedVersion: DiscordVersion,
debuggable: Boolean,
setDebuggable: (Boolean) -> Unit,
replaceIcon: Boolean,
setReplaceIcon: (Boolean) -> Unit,
appName: String,
appNameIsError: Boolean,
setAppName: (String) -> Unit,
packageName: String,
packageNameState: PackageNameState,
setPackageName: (String) -> Unit,
isConfigValid: Boolean,
onInstall: () -> Unit,
) {
Scaffold(
topBar = { PatchOptionsAppBar(isUpdate = isUpdate) },
) { paddingValues ->
Column(
verticalArrangement = Arrangement.spacedByLastAtBottom(20.dp),
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(paddingValues)
.padding(horizontal = 20.dp, vertical = 10.dp)
) {
Text(
text = stringResource(R.string.patchopts_title),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
)
TextDivider(text = stringResource(R.string.patchopts_divider_basic))
SwitchPatchOption(
icon = painterResource(R.drawable.ic_app_shortcut),
name = stringResource(R.string.patchopts_icon_title),
description = stringResource(R.string.patchopts_icon_desc),
value = replaceIcon,
onValueChange = setReplaceIcon,
modifier = Modifier.padding(bottom = 8.dp)
)
val appNameIsDefault by remember {
derivedStateOf {
appName == PatchOptions.Default.appName
}
AnimatedVersionDisplay(
version = animatedVersion,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.offset(y = (-10).dp),
)
}
TextPatchOption(
name = stringResource(R.string.patchopts_appname_title),
description = stringResource(R.string.patchopts_appname_desc),
value = appName,
valueIsError = appNameIsError,
valueIsDefault = appNameIsDefault,
onValueChange = setAppName,
onValueReset = { setAppName(PatchOptions.Default.appName) },
)
TextDivider(text = stringResource(R.string.patchopts_divider_basic))
val packageNameIsDefault by remember {
derivedStateOf {
packageName == PatchOptions.Default.packageName
}
}
TextPatchOption(
name = stringResource(R.string.patchopts_pkgname_title),
description = stringResource(R.string.patchopts_pkgname_desc),
value = packageName,
valueIsError = packageNameState == PackageNameState.Invalid,
valueIsDefault = packageNameIsDefault,
onValueChange = setPackageName,
onValueReset = { setPackageName(PatchOptions.Default.packageName) },
) {
PackageNameStateLabel(
state = packageNameState,
modifier = Modifier.padding(start = 4.dp),
)
}
if (isDevMode) {
TextDivider(
text = stringResource(R.string.patchopts_divider_advanced),
modifier = Modifier.padding(top = 12.dp),
)
SwitchPatchOption(
icon = painterResource(R.drawable.ic_app_shortcut),
name = stringResource(R.string.patchopts_icon_title),
description = stringResource(R.string.patchopts_icon_desc),
value = model.replaceIcon,
onValueChange = model::changeReplaceIcon,
modifier = Modifier.padding(bottom = 8.dp)
icon = painterResource(R.drawable.ic_bug),
name = stringResource(R.string.patchopts_debuggable_title),
description = stringResource(R.string.patchopts_debuggable_desc),
value = debuggable,
onValueChange = setDebuggable,
)
}
TextPatchOption(
name = stringResource(R.string.patchopts_appname_title),
description = stringResource(R.string.patchopts_appname_desc),
value = model.appName,
valueIsError = model.appNameIsError,
valueIsDefault = model.appNameIsDefault,
onValueChange = model::changeAppName,
onValueReset = model::resetAppName
)
VersionDisplay(
version = supportedVersion,
prefix = {
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
append(stringResource(R.string.version_supported))
append(" ")
}
},
modifier = Modifier
.alpha(.5f)
.align(Alignment.CenterHorizontally)
.padding(top = 20.dp)
)
TextPatchOption(
name = stringResource(R.string.patchopts_pkgname_title),
description = stringResource(R.string.patchopts_pkgname_desc),
value = model.packageName,
valueIsError = model.packageNameState == PackageNameState.Invalid,
valueIsDefault = model.packageNameIsDefault,
onValueChange = model::changePackageName,
onValueReset = model::resetPackageName,
) {
PackageNameState(
state = model.packageNameState,
modifier = Modifier.padding(start = 4.dp),
)
}
Spacer(Modifier.weight(1f))
if (model.isDevMode) {
TextDivider(
text = stringResource(R.string.patchopts_divider_advanced),
modifier = Modifier.padding(top = 12.dp),
)
SwitchPatchOption(
icon = painterResource(R.drawable.ic_bug),
name = stringResource(R.string.patchopts_debuggable_title),
description = stringResource(R.string.patchopts_debuggable_desc),
value = model.debuggable,
onValueChange = model::changeDebuggable,
)
}
Spacer(Modifier.weight(1f))
FilledTonalButton(
enabled = model.isConfigValid,
onClick = { navigator.push(PatchingScreen(model.generateConfig())) },
colors = ButtonDefaults.filledTonalButtonColors(
contentColor = MaterialTheme.colorScheme.primary,
),
modifier = Modifier
.padding(bottom = 10.dp)
.align(Alignment.End),
) {
Text(stringResource(R.string.action_install))
}
FilledTonalButton(
enabled = isConfigValid,
onClick = onInstall,
colors = ButtonDefaults.filledTonalButtonColors(
contentColor = MaterialTheme.colorScheme.primary,
),
modifier = Modifier
.padding(bottom = 10.dp)
.align(Alignment.End),
) {
Text(stringResource(R.string.action_install))
}
}
}

View file

@ -15,7 +15,7 @@ import com.aliucord.manager.R
import com.aliucord.manager.ui.screens.patchopts.PackageNameState
@Composable
fun PackageNameState(
fun PackageNameStateLabel(
state: PackageNameState,
modifier: Modifier = Modifier,
) {

View file

@ -5,7 +5,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.aliucord.manager.R
import com.aliucord.manager.ui.components.BackButton
import com.aliucord.manager.ui.screens.settings.SettingsScreen
@ -18,9 +17,9 @@ fun PatchOptionsAppBar(
navigationIcon = { BackButton() },
title = { Text(stringResource(if (!isUpdate) R.string.action_add_install else R.string.action_update_install)) },
actions = {
val navigator = LocalNavigator.currentOrThrow
val navigator = LocalNavigator.current
IconButton(onClick = { navigator.push(SettingsScreen()) }) {
IconButton(onClick = { navigator?.push(SettingsScreen()) }) {
Icon(
painter = painterResource(R.drawable.ic_settings),
contentDescription = stringResource(R.string.navigation_settings)

View file

@ -16,8 +16,8 @@
<string name="action_dismiss">Dismiss</string>
<string name="action_install">Install</string>
<string name="action_uninstall">Uninstall</string>
<string name="action_add_install">New Aliucord Installation</string>
<string name="action_update_install">Update Aliucord Installation</string>
<string name="action_add_install">New install</string>
<string name="action_update_install">Update install</string>
<string name="action_update">Update</string>
<string name="action_clear">Clear</string>
<string name="action_launch">Launch</string>