mirror of
https://github.com/rhunk/SnapEnhance.git
synced 2025-06-13 13:47:47 +02:00
ui(features): dialogs & translations
This commit is contained in:
@ -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"))
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
|
@ -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 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user