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

View File

@ -18,159 +18,173 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
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.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import me.rhunk.snapenhance.config.ConfigProperty import me.rhunk.snapenhance.config.ConfigProperty
import me.rhunk.snapenhance.config.impl.ConfigIntegerValue import me.rhunk.snapenhance.config.impl.ConfigIntegerValue
import me.rhunk.snapenhance.config.impl.ConfigStateListValue import me.rhunk.snapenhance.config.impl.ConfigStateListValue
import me.rhunk.snapenhance.config.impl.ConfigStateSelection import me.rhunk.snapenhance.config.impl.ConfigStateSelection
import me.rhunk.snapenhance.config.impl.ConfigStringValue import me.rhunk.snapenhance.manager.data.ManagerContext
@Composable class Dialogs(
@Preview private val context: ManagerContext
fun TestPreview() { ) {
KeyboardInputDialog(config = ConfigProperty.SAVE_FOLDER) @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 @Composable
fun DefaultDialogCard(content: @Composable ColumnScope.() -> Unit) { fun DefaultEntryText(text: String, modifier: Modifier = Modifier) {
Card( Text(
shape = MaterialTheme.shapes.medium, text = text,
modifier = Modifier
.padding(10.dp, 5.dp, 10.dp, 10.dp),
) {
Column(
modifier = Modifier modifier = Modifier
.padding(10.dp, 10.dp, 10.dp, 10.dp) .padding(10.dp, 10.dp, 10.dp, 10.dp)
.verticalScroll(ScrollState(0)), .then(modifier)
) { 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
) )
}
Row( @Composable
modifier = Modifier.padding(top = 10.dp).fillMaxWidth(), fun StateSelectionDialog(config: ConfigProperty) {
horizontalArrangement = Arrangement.SpaceEvenly, assert(config.valueContainer is ConfigStateSelection)
) { val keys = (config.valueContainer as ConfigStateSelection).keys()
Button(onClick = { dismiss() }) { val selectedValue = remember {
Text(text = "Cancel") mutableStateOf(config.valueContainer.value())
} }
Button(onClick = { DefaultDialogCard {
config.valueContainer.writeFrom(fieldValue.value) keys.forEach { item ->
dismiss() fun select() {
}) { selectedValue.value = item
Text(text = "Ok") 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) { @Composable
focusRequester.requestFocus() fun KeyboardInputDialog(config: ConfigProperty, dismiss: () -> Unit = {}) {
} val focusRequester = remember { FocusRequester() }
}
@Composable DefaultDialogCard {
fun StateListDialog(config: ConfigProperty) { val fieldValue = remember {
assert(config.valueContainer is ConfigStateListValue) mutableStateOf(config.valueContainer.read().let {
val stateList = (config.valueContainer as ConfigStateListValue).value() TextFieldValue(
DefaultDialogCard { text = it,
stateList.keys.forEach { key -> selection = TextRange(it.length)
val state = remember { )
mutableStateOf(stateList[key] ?: false) })
} }
fun toggle(value: Boolean? = null) { TextField(
state.value = value ?: !state.value modifier = Modifier
stateList[key] = state.value .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( Row(
modifier = Modifier.clickable { toggle() }, modifier = Modifier.padding(top = 10.dp).fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically horizontalArrangement = Arrangement.SpaceEvenly,
) { ) {
DefaultEntryText( Button(onClick = { dismiss() }) {
text = key, Text(text = "Cancel")
modifier = Modifier }
.weight(1f) Button(onClick = {
) config.valueContainer.writeFrom(fieldValue.value.text)
Switch( dismiss()
checked = state.value, }) {
onCheckedChange = { Text(text = "Ok")
toggle(it) }
}
)
} }
} }
} }
}
@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 android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent 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.MaterialTheme
import androidx.compose.material3.Scaffold 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 androidx.navigation.compose.rememberNavController
import me.rhunk.snapenhance.manager.data.ManagerContext import me.rhunk.snapenhance.manager.data.ManagerContext
@ -27,26 +13,20 @@ class MainActivity : ComponentActivity() {
@SuppressLint("UnusedMaterialScaffoldPaddingParameter") @SuppressLint("UnusedMaterialScaffoldPaddingParameter")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val startDestination = intent.getStringExtra("route")?.let { EnumSection.fromRoute(it) } ?: EnumSection.HOME
val managerContext = ManagerContext(this) val managerContext = ManagerContext(this)
setContent { 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 @Composable
fun NavigationHost( fun NavigationHost(
startDestination: EnumSection,
navController: NavHostController, navController: NavHostController,
innerPadding: PaddingValues innerPadding: PaddingValues
) { ) {
@ -40,7 +41,7 @@ class Navigation(
instance.navController = navController instance.navController = navController
} } } }
NavHost(navController, startDestination = EnumSection.FEATURES.route, Modifier.padding(innerPadding)) { NavHost(navController, startDestination = startDestination.route, Modifier.padding(innerPadding)) {
sections.forEach { (section, instance) -> sections.forEach { (section, instance) ->
composable(section.route) { composable(section.route) {
instance.Content() instance.Content()

View File

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

View File

@ -1,13 +1,8 @@
package me.rhunk.snapenhance.manager.sections package me.rhunk.snapenhance.manager.sections
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable 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 import me.rhunk.snapenhance.manager.Section
class NotImplemented : Section() { class NotImplemented : Section() {

View File

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

View File

@ -48,9 +48,10 @@ class SettingsGearInjector : AbstractMenu() {
setOnClickListener { setOnClickListener {
val intent = Intent().apply { 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) context.startActivity(intent)
} }