ui(features): dialogs & translations

This commit is contained in:
rhunk
2023-07-31 00:53:55 +02:00
parent d3434a4be2
commit 5d4e2aacb1
9 changed files with 190 additions and 181 deletions

View File

@ -22,8 +22,7 @@ android {
defaultConfig {
applicationId = rootProject.ext["applicationId"].toString()
minSdk = 28
//noinspection OldTargetApi
targetSdk = 33
targetSdk = 34
multiDexEnabled = true
}
@ -37,11 +36,12 @@ android {
flavorDimensions += "abi"
productFlavors {
create("armv8") {
ndk {
abiFilters.add("arm64-v8a")
}
dimension = "abi"
}
@ -81,13 +81,13 @@ android {
dependencies {
implementation(project(":core"))
implementation(libs.androidx.material.icons.extended)
implementation(libs.androidx.navigation.compose)
implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.material3)
implementation(libs.androidx.material)
implementation(libs.androidx.activity.ktx)
implementation(libs.androidx.navigation.compose)
debugImplementation("androidx.compose.ui:ui-tooling:1.4.3")
implementation("androidx.compose.ui:ui-tooling-preview:1.4.3")
debugImplementation("androidx.compose.ui:ui-tooling-preview:1.4.3")
implementation(kotlin("reflect"))
}

View File

@ -18,159 +18,173 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.config.impl.ConfigIntegerValue
import me.rhunk.snapenhance.config.impl.ConfigStateListValue
import me.rhunk.snapenhance.config.impl.ConfigStateSelection
import me.rhunk.snapenhance.config.impl.ConfigStringValue
import me.rhunk.snapenhance.manager.data.ManagerContext
@Composable
@Preview
fun TestPreview() {
KeyboardInputDialog(config = ConfigProperty.SAVE_FOLDER)
}
class Dialogs(
private val context: ManagerContext
) {
@Composable
fun DefaultDialogCard(content: @Composable ColumnScope.() -> Unit) {
Card(
shape = MaterialTheme.shapes.medium,
modifier = Modifier
.padding(10.dp, 5.dp, 10.dp, 10.dp),
) {
Column(
modifier = Modifier
.padding(10.dp, 10.dp, 10.dp, 10.dp)
.verticalScroll(ScrollState(0)),
) { content() }
}
}
@Composable
fun DefaultDialogCard(content: @Composable ColumnScope.() -> Unit) {
Card(
shape = MaterialTheme.shapes.medium,
modifier = Modifier
.padding(10.dp, 5.dp, 10.dp, 10.dp),
) {
Column(
@Composable
fun DefaultEntryText(text: String, modifier: Modifier = Modifier) {
Text(
text = text,
modifier = Modifier
.padding(10.dp, 10.dp, 10.dp, 10.dp)
.verticalScroll(ScrollState(0)),
) { content() }
}
}
@Composable
fun DefaultEntryText(text: String, modifier: Modifier = Modifier) {
Text(
text = text,
modifier = Modifier
.padding(10.dp, 10.dp, 10.dp, 10.dp)
.then(modifier)
)
}
@Composable
fun StateSelectionDialog(config: ConfigProperty) {
assert(config.valueContainer is ConfigStateSelection)
val keys = (config.valueContainer as ConfigStateSelection).keys()
val selectedValue = remember {
mutableStateOf(config.valueContainer.value())
}
DefaultDialogCard {
keys.forEach { item ->
fun select() {
selectedValue.value = item
config.valueContainer.writeFrom(item)
}
Row(
modifier = Modifier.clickable { select() },
verticalAlignment = Alignment.CenterVertically
) {
DefaultEntryText(
text = item,
modifier = Modifier.weight(1f)
)
RadioButton(
selected = selectedValue.value == item,
onClick = { select() }
)
}
}
}
}
@Composable
fun KeyboardInputDialog(config: ConfigProperty, dismiss: () -> Unit = {}) {
val focusRequester = remember { FocusRequester() }
DefaultDialogCard {
val fieldValue = remember { mutableStateOf(config.valueContainer.read()) }
TextField(
modifier = Modifier.fillMaxWidth().padding(all = 10.dp).focusRequester(focusRequester),
value = fieldValue.value, onValueChange = {
fieldValue.value = it
},
keyboardOptions = when (config.valueContainer) {
is ConfigIntegerValue -> {
KeyboardOptions(keyboardType = KeyboardType.Number)
}
else -> {
KeyboardOptions(keyboardType = KeyboardType.Text)
}
},
singleLine = true
.then(modifier)
)
}
Row(
modifier = Modifier.padding(top = 10.dp).fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
) {
Button(onClick = { dismiss() }) {
Text(text = "Cancel")
}
Button(onClick = {
config.valueContainer.writeFrom(fieldValue.value)
dismiss()
}) {
Text(text = "Ok")
@Composable
fun StateSelectionDialog(config: ConfigProperty) {
assert(config.valueContainer is ConfigStateSelection)
val keys = (config.valueContainer as ConfigStateSelection).keys()
val selectedValue = remember {
mutableStateOf(config.valueContainer.value())
}
DefaultDialogCard {
keys.forEach { item ->
fun select() {
selectedValue.value = item
config.valueContainer.writeFrom(item)
}
Row(
modifier = Modifier.clickable { select() },
verticalAlignment = Alignment.CenterVertically
) {
DefaultEntryText(
text = if (config.disableValueLocalization)
item
else context.translation.propertyOption(config, item),
modifier = Modifier.weight(1f)
)
RadioButton(
selected = selectedValue.value == item,
onClick = { select() }
)
}
}
}
}
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
@Composable
fun KeyboardInputDialog(config: ConfigProperty, dismiss: () -> Unit = {}) {
val focusRequester = remember { FocusRequester() }
@Composable
fun StateListDialog(config: ConfigProperty) {
assert(config.valueContainer is ConfigStateListValue)
val stateList = (config.valueContainer as ConfigStateListValue).value()
DefaultDialogCard {
stateList.keys.forEach { key ->
val state = remember {
mutableStateOf(stateList[key] ?: false)
DefaultDialogCard {
val fieldValue = remember {
mutableStateOf(config.valueContainer.read().let {
TextFieldValue(
text = it,
selection = TextRange(it.length)
)
})
}
fun toggle(value: Boolean? = null) {
state.value = value ?: !state.value
stateList[key] = state.value
}
TextField(
modifier = Modifier
.fillMaxWidth()
.padding(all = 10.dp)
.onGloballyPositioned {
focusRequester.requestFocus()
}
.focusRequester(focusRequester),
value = fieldValue.value,
onValueChange = {
fieldValue.value = it
},
keyboardOptions = when (config.valueContainer) {
is ConfigIntegerValue -> {
KeyboardOptions(keyboardType = KeyboardType.Number)
}
else -> {
KeyboardOptions(keyboardType = KeyboardType.Text)
}
},
singleLine = true
)
Row(
modifier = Modifier.clickable { toggle() },
verticalAlignment = Alignment.CenterVertically
modifier = Modifier.padding(top = 10.dp).fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly,
) {
DefaultEntryText(
text = key,
modifier = Modifier
.weight(1f)
)
Switch(
checked = state.value,
onCheckedChange = {
toggle(it)
}
)
Button(onClick = { dismiss() }) {
Text(text = "Cancel")
}
Button(onClick = {
config.valueContainer.writeFrom(fieldValue.value.text)
dismiss()
}) {
Text(text = "Ok")
}
}
}
}
}
@Composable
fun StateListDialog(config: ConfigProperty) {
assert(config.valueContainer is ConfigStateListValue)
val stateList = (config.valueContainer as ConfigStateListValue).value()
DefaultDialogCard {
stateList.keys.forEach { key ->
val state = remember {
mutableStateOf(stateList[key] ?: false)
}
fun toggle(value: Boolean? = null) {
state.value = value ?: !state.value
stateList[key] = state.value
}
Row(
modifier = Modifier.clickable { toggle() },
verticalAlignment = Alignment.CenterVertically
) {
DefaultEntryText(
text = if (config.disableValueLocalization)
key
else context.translation.propertyOption(config, key),
modifier = Modifier
.weight(1f)
)
Switch(
checked = state.value,
onCheckedChange = {
toggle(it)
}
)
}
}
}
}
}

View File

@ -4,22 +4,8 @@ import android.annotation.SuppressLint
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.navigation
import androidx.navigation.compose.rememberNavController
import me.rhunk.snapenhance.manager.data.ManagerContext
@ -27,26 +13,20 @@ class MainActivity : ComponentActivity() {
@SuppressLint("UnusedMaterialScaffoldPaddingParameter")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val startDestination = intent.getStringExtra("route")?.let { EnumSection.fromRoute(it) } ?: EnumSection.HOME
val managerContext = ManagerContext(this)
setContent {
App(managerContext)
val navController = rememberNavController()
val navigation = Navigation(managerContext)
AppMaterialTheme {
Scaffold(
containerColor = MaterialTheme.colorScheme.background,
bottomBar = { navigation.NavBar(navController = navController) }
) { innerPadding ->
navigation.NavigationHost(navController = navController, innerPadding = innerPadding, startDestination = startDestination)
}
}
}
}
}
@Composable
fun App(
context: ManagerContext
) {
val navController = rememberNavController()
val navigation = Navigation(context)
AppMaterialTheme {
Scaffold(
containerColor = MaterialTheme.colorScheme.background,
bottomBar = { navigation.NavBar(navController = navController) }
) { innerPadding ->
navigation.NavigationHost(navController = navController, innerPadding = innerPadding)
}
}
}

View File

@ -30,6 +30,7 @@ class Navigation(
) {
@Composable
fun NavigationHost(
startDestination: EnumSection,
navController: NavHostController,
innerPadding: PaddingValues
) {
@ -40,7 +41,7 @@ class Navigation(
instance.navController = navController
} }
NavHost(navController, startDestination = EnumSection.FEATURES.route, Modifier.padding(innerPadding)) {
NavHost(navController, startDestination = startDestination.route, Modifier.padding(innerPadding)) {
sections.forEach { (section, instance) ->
composable(section.route) {
instance.Content()

View File

@ -48,6 +48,12 @@ enum class EnumSection(
title = "Debug",
icon = Icons.Filled.BugReport
);
companion object {
fun fromRoute(route: String): EnumSection {
return values().first { it.route == route }
}
}
}

View File

@ -4,9 +4,12 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
@ -31,6 +34,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@ -43,15 +47,15 @@ import me.rhunk.snapenhance.config.impl.ConfigStateListValue
import me.rhunk.snapenhance.config.impl.ConfigStateSelection
import me.rhunk.snapenhance.config.impl.ConfigStateValue
import me.rhunk.snapenhance.config.impl.ConfigStringValue
import me.rhunk.snapenhance.manager.StateListDialog
import me.rhunk.snapenhance.manager.Dialogs
import me.rhunk.snapenhance.manager.Section
import me.rhunk.snapenhance.manager.StateSelectionDialog
import me.rhunk.snapenhance.manager.KeyboardInputDialog
typealias ClickCallback = (Boolean) -> Unit
typealias RegisterClickCallback = (ClickCallback) -> ClickCallback
class FeaturesSection : Section() {
private val dialogs by lazy { Dialogs(manager) }
@Composable
private fun PropertyAction(item: ConfigProperty, registerClickCallback: RegisterClickCallback) {
val showDialog = remember { mutableStateOf(false) }
@ -82,12 +86,15 @@ class FeaturesSection : Section() {
is ConfigStateSelection -> {
registerDialogOnClickCallback()
dialogComposable.value = {
StateSelectionDialog(item)
dialogs.StateSelectionDialog(item)
}
Text(
text = container.value().let {
it.substring(0, it.length.coerceAtMost(20))
}
overflow = TextOverflow.Ellipsis,
maxLines = 1,
modifier = Modifier.widthIn(0.dp, 120.dp),
text = if (item.disableValueLocalization) container.value() else {
manager.translation.propertyOption(item, container.value())
},
)
}
@ -95,10 +102,10 @@ class FeaturesSection : Section() {
dialogComposable.value = {
when (container) {
is ConfigStateListValue -> {
StateListDialog(item)
dialogs.StateListDialog(item)
}
is ConfigStringValue, is ConfigIntegerValue -> {
KeyboardInputDialog(item) { showDialog.value = false }
dialogs.KeyboardInputDialog(item) { showDialog.value = false }
}
}
}
@ -106,7 +113,7 @@ class FeaturesSection : Section() {
registerDialogOnClickCallback().let { { it.invoke(true) } }.also {
if (container is ConfigIntegerValue) {
FilledIconButton(onClick = it) {
Text(text = container.value().toString())
Text(text = container.value().toString(), modifier = Modifier.wrapContentWidth(), overflow = TextOverflow.Ellipsis)
}
} else {
IconButton(onClick = it) {
@ -132,7 +139,7 @@ class FeaturesSection : Section() {
Row(
modifier = Modifier
.fillMaxSize()
.padding(all = 10.dp),
.padding(all = 4.dp),
horizontalArrangement = Arrangement.SpaceBetween
) {
Column(
@ -206,10 +213,11 @@ class FeaturesSection : Section() {
)
LazyColumn(
modifier = Modifier
.fillMaxSize(),
.fillMaxHeight(),
verticalArrangement = Arrangement.Center
) {
items(configItems) { item ->
if (item.shouldAppearInSettings.not()) return@items
PropertyCard(item)
}
}

View File

@ -1,13 +1,8 @@
package me.rhunk.snapenhance.manager.sections
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import me.rhunk.snapenhance.manager.Section
class NotImplemented : Section() {

View File

@ -88,6 +88,10 @@ class TranslationWrapper {
return get("property.${property.translationKey}.description")
}
fun propertyOption(property: ConfigProperty, item: String): String {
return get(property.getOptionTranslationKey(item))
}
fun format(key: String, vararg args: Pair<String, String>): String {
return args.fold(get(key)) { acc, pair ->
acc.replace("{${pair.first}}", pair.second)

View File

@ -48,9 +48,10 @@ class SettingsGearInjector : AbstractMenu() {
setOnClickListener {
val intent = Intent().apply {
setClassName(BuildConfig.APPLICATION_ID, ConfigActivity::class.java.name)
setClassName(BuildConfig.APPLICATION_ID, "me.rhunk.snapenhance.manager.MainActivity")
putExtra("route", "features")
putExtra("lspatched", File(context.cacheDir, "lspatch/origin").exists())
}
intent.putExtra("lspatched", File(context.cacheDir, "lspatch/origin").exists())
context.startActivity(intent)
}