diff --git a/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/components/PanelDialog.kt b/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/components/PanelDialog.kt new file mode 100644 index 00000000..0ab0b789 --- /dev/null +++ b/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/components/PanelDialog.kt @@ -0,0 +1,46 @@ +package com.redmadrobot.debug.uikit.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.redmadrobot.debug.uikit.theme.DebugPanelShapes +import com.redmadrobot.debug.uikit.theme.DebugPanelTheme + +@Composable +public fun PanelDialog( + title: String, + onDismiss: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable ColumnScope.() -> Unit, +) { + Dialog( + onDismissRequest = onDismiss, + properties = DialogProperties(usePlatformDefaultWidth = false), + ) { + Surface( + shape = DebugPanelShapes.dialog, + color = DebugPanelTheme.colors.surface.dialog, + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 24.dp), + ) { + Column(modifier = Modifier.padding(all = 24.dp)) { + Text( + text = title, + style = DebugPanelTheme.typography.titleLarge, + color = DebugPanelTheme.colors.content.primary, + modifier = Modifier.padding(bottom = 16.dp), + ) + content() + } + } + } +} diff --git a/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/components/PanelSearchBar.kt b/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/components/PanelSearchBar.kt new file mode 100644 index 00000000..066ac3ac --- /dev/null +++ b/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/components/PanelSearchBar.kt @@ -0,0 +1,94 @@ +package com.redmadrobot.debug.uikit.components + +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.redmadrobot.debug.uikit.R +import com.redmadrobot.debug.uikit.theme.DebugPanelDimensions +import com.redmadrobot.debug.uikit.theme.DebugPanelShapes +import com.redmadrobot.debug.uikit.theme.DebugPanelTheme +import com.redmadrobot.debug.uikit.theme.MonoFontFamily + +@Suppress("LongMethod") +@Composable +public fun PanelSearchBar( + query: String, + placeholder: String, + onQueryChange: (String) -> Unit, + modifier: Modifier = Modifier, +) { + val minHeight by animateDpAsState( + targetValue = if (query.isNotEmpty()) 48.dp else 40.dp, + label = "", + ) + + Row( + modifier = modifier + .fillMaxWidth() + .heightIn(min = minHeight) + .background( + color = DebugPanelTheme.colors.surface.secondary, + shape = DebugPanelShapes.medium, + ) + .padding(horizontal = 12.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = painterResource(R.drawable.icon_search), + contentDescription = null, + tint = DebugPanelTheme.colors.content.tertiary, + modifier = Modifier.size(size = DebugPanelDimensions.iconSizeSmall), + ) + BasicTextField( + value = query, + onValueChange = onQueryChange, + modifier = Modifier + .weight(weight = 1f) + .padding(horizontal = 8.dp), + textStyle = DebugPanelTheme.typography.bodyMedium.copy( + fontFamily = MonoFontFamily, + color = DebugPanelTheme.colors.content.primary, + ), + singleLine = true, + cursorBrush = SolidColor(value = DebugPanelTheme.colors.content.accent), + decorationBox = { innerTextField -> + if (query.isEmpty()) { + Text( + text = placeholder, + style = DebugPanelTheme.typography.bodyMedium, + color = DebugPanelTheme.colors.content.tertiary, + ) + } + innerTextField() + }, + ) + if (query.isNotEmpty()) { + IconButton( + onClick = { onQueryChange("") }, + modifier = Modifier.size(size = DebugPanelDimensions.iconSizeLarge), + ) { + Icon( + painter = painterResource(R.drawable.icon_clear), + contentDescription = null, + tint = DebugPanelTheme.colors.content.tertiary, + modifier = Modifier.size(size = DebugPanelDimensions.iconSizeSmall), + ) + } + } + } +} diff --git a/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/components/PanelStyledTextField.kt b/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/components/PanelStyledTextField.kt new file mode 100644 index 00000000..28c9f38d --- /dev/null +++ b/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/components/PanelStyledTextField.kt @@ -0,0 +1,65 @@ +package com.redmadrobot.debug.uikit.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TextFieldColors +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.dp +import com.redmadrobot.debug.uikit.theme.DebugPanelTheme + +private val defaultTextStyle: TextStyle + @Composable get() = DebugPanelTheme.typography.bodyMedium.copy( + color = DebugPanelTheme.colors.content.primary, + ) + +private val defaultColors: TextFieldColors + @Composable get() = OutlinedTextFieldDefaults.colors( + focusedBorderColor = DebugPanelTheme.colors.content.accent, + unfocusedBorderColor = DebugPanelTheme.colors.stroke.secondary, + focusedLabelColor = DebugPanelTheme.colors.content.accent, + unfocusedLabelColor = DebugPanelTheme.colors.content.tertiary, + errorBorderColor = DebugPanelTheme.colors.content.error, + cursorColor = DebugPanelTheme.colors.content.accent, + ) + +@Composable +public fun PanelStyledTextField( + value: String, + label: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + isError: Boolean = false, + errorMessage: String? = null, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + textStyle: TextStyle = defaultTextStyle, + colors: TextFieldColors = defaultColors +) { + Column(modifier = modifier.fillMaxWidth()) { + OutlinedTextField( + value = value, + onValueChange = onValueChange, + modifier = Modifier.fillMaxWidth(), + label = { Text(text = label, style = DebugPanelTheme.typography.bodyMedium) }, + isError = isError, + singleLine = true, + keyboardOptions = keyboardOptions, + textStyle = textStyle, + colors = colors, + ) + if (isError && errorMessage != null) { + Text( + text = errorMessage, + style = DebugPanelTheme.typography.bodySmall, + color = DebugPanelTheme.colors.content.error, + modifier = Modifier.padding(top = 4.dp), + ) + } + } +} diff --git a/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/components/PanelToggle.kt b/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/components/PanelToggle.kt new file mode 100644 index 00000000..64416640 --- /dev/null +++ b/panel-ui-kit/src/main/kotlin/com/redmadrobot/debug/uikit/components/PanelToggle.kt @@ -0,0 +1,50 @@ +package com.redmadrobot.debug.uikit.components + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.redmadrobot.debug.uikit.theme.DebugPanelDimensions +import com.redmadrobot.debug.uikit.theme.DebugPanelTheme + +@Composable +public fun PanelToggle( + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + val (trackColor, thumbOffset) = with(DebugPanelTheme.colors) { + if (checked) content.teal to 22.dp else stroke.primary to 2.dp + } + val animatedTrackColor by animateColorAsState(targetValue = trackColor, label = "") + val animatedThumbOffset by animateDpAsState(targetValue = thumbOffset, label = "") + + Box( + modifier = modifier + .width(width = DebugPanelDimensions.toggleWidth) + .height(height = DebugPanelDimensions.toggleHeight) + .clip(shape = RoundedCornerShape(size = 12.dp)) + .background(color = animatedTrackColor) + .clickable { onCheckedChange(!checked) }, + ) { + Box( + modifier = Modifier + .padding(start = animatedThumbOffset, top = 2.dp) + .size(size = 20.dp) + .background(color = Color.White, shape = CircleShape), + ) + } +} diff --git a/panel-ui-kit/src/main/res/drawable/icon_clear.xml b/panel-ui-kit/src/main/res/drawable/icon_clear.xml new file mode 100644 index 00000000..95cc170f --- /dev/null +++ b/panel-ui-kit/src/main/res/drawable/icon_clear.xml @@ -0,0 +1,10 @@ + + + diff --git a/panel-ui-kit/src/main/res/drawable/icon_search.xml b/panel-ui-kit/src/main/res/drawable/icon_search.xml new file mode 100644 index 00000000..afe352ee --- /dev/null +++ b/panel-ui-kit/src/main/res/drawable/icon_search.xml @@ -0,0 +1,10 @@ + + + diff --git a/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/EditConfigValueDialog.kt b/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/EditConfigValueDialog.kt index 9a6bdbda..6d3cc923 100644 --- a/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/EditConfigValueDialog.kt +++ b/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/EditConfigValueDialog.kt @@ -1,15 +1,15 @@ package com.redmadrobot.debug.plugin.konfeature.ui +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.height import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.AlertDialog -import androidx.compose.material.Button -import androidx.compose.material.Checkbox -import androidx.compose.material.OutlinedTextField -import androidx.compose.material.Text +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue @@ -18,13 +18,16 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import com.redmadrobot.debug.plugin.konfeature.R import com.redmadrobot.debug.plugin.konfeature.ui.data.EditDialogState -import com.redmadrobot.debug.core.R as CoreR +import com.redmadrobot.debug.uikit.components.PanelDialog +import com.redmadrobot.debug.uikit.components.PanelStyledTextField +import com.redmadrobot.debug.uikit.components.PanelToggle +import com.redmadrobot.debug.uikit.theme.DebugPanelShapes +import com.redmadrobot.debug.uikit.theme.DebugPanelTheme @Composable internal fun EditConfigValueDialog( @@ -32,6 +35,7 @@ internal fun EditConfigValueDialog( onValueChange: (key: String, value: Any) -> Unit, onValueReset: (key: String) -> Unit, onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, ) { val initialValue = state.value var value by remember { mutableStateOf(state.value) } @@ -40,41 +44,40 @@ internal fun EditConfigValueDialog( derivedStateOf { !isInputEmpty && initialValue != value } } - AlertDialog( - backgroundColor = colorResource(id = CoreR.color.super_light_gray), - title = { - Text(text = stringResource(id = R.string.konfeature_plugin_edit_dialog_title, state.key)) - }, - text = { - when (initialValue) { - is Boolean -> BooleanEditInput(initialValue, onValueChange = { value = it }) - is Long -> LongEditInput( - initialValue, - onValueChange = { value = it }, - onEmptyInput = { isInputEmpty = it } - ) - - is Double -> DoubleEditInput( - initialValue, - onValueChange = { value = it }, - onEmptyImput = { isInputEmpty = it } - ) - - is String -> StringEditInput(initialValue, onValueChange = { value = it }) - } - }, - onDismissRequest = onDismissRequest, - buttons = { - EditConfigValueButtons( - state = state, - saveEnabled = saveEnabled, - value = value, - onValueChange = onValueChange, - onValueReset = onValueReset, - onDismissRequest = onDismissRequest, + PanelDialog(title = state.key, onDismiss = onDismissRequest, modifier = modifier) { + when (initialValue) { + is Boolean -> BooleanEditInput( + value = initialValue, + onValueChange = { value = it }, + ) + + is Long -> LongEditInput( + value = initialValue, + onValueChange = { value = it }, + onEmptyInput = { isInputEmpty = it }, + ) + + is Double -> DoubleEditInput( + value = initialValue, + onValueChange = { value = it }, + onEmptyInput = { isInputEmpty = it }, + ) + + is String -> StringEditInput( + value = initialValue, + onValueChange = { value = it }, ) } - ) + Spacer(modifier = Modifier.height(20.dp)) + EditConfigValueButtons( + state = state, + saveEnabled = saveEnabled, + value = value, + onValueChange = onValueChange, + onValueReset = onValueReset, + onDismissRequest = onDismissRequest, + ) + } } @Composable @@ -85,58 +88,114 @@ private fun EditConfigValueButtons( onValueChange: (key: String, value: Any) -> Unit, onValueReset: (key: String) -> Unit, onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, ) { Row( - modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp) + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(space = 8.dp), + verticalAlignment = Alignment.CenterVertically, ) { - Button(onClick = onDismissRequest) { - Text(text = stringResource(id = R.string.konfeature_plugin_close)) - } - Spacer(modifier = Modifier.weight(1f)) - Button( - enabled = saveEnabled, - onClick = { - onValueChange.invoke(state.key, value) - onDismissRequest.invoke() - } - ) { - Text(text = stringResource(id = R.string.konfeature_plugin_save)) - } + CloseButton(onDismissRequest = onDismissRequest) + Spacer(modifier = Modifier.weight(weight = 1f)) if (state.isDebugSource) { - Button( - modifier = Modifier.padding(start = 8.dp), - onClick = { - onValueReset.invoke(state.key) - onDismissRequest.invoke() - } - ) { - Text(text = stringResource(id = R.string.konfeature_plugin_reset)) - } + DebugSourceButton( + onValueReset = { onValueReset.invoke(state.key) }, + onDismissRequest = onDismissRequest + ) } + SaveButton( + saveEnabled = saveEnabled, + onValueChange = { onValueChange.invoke(state.key, value) }, + onDismissRequest = onDismissRequest + ) + } +} + +@Composable +private fun SaveButton( + saveEnabled: Boolean, + onValueChange: () -> Unit, + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, +) { + Button( + modifier = modifier, + onClick = { + onValueChange() + onDismissRequest() + }, + enabled = saveEnabled, + shape = DebugPanelShapes.medium, + ) { + Text( + text = stringResource(R.string.konfeature_plugin_save), + style = DebugPanelTheme.typography.labelLarge, + ) + } +} + +@Composable +private fun DebugSourceButton( + onValueReset: () -> Unit, + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, +) { + OutlinedButton( + modifier = modifier, + onClick = { + onValueReset() + onDismissRequest() + }, + shape = DebugPanelShapes.medium, + colors = ButtonDefaults.outlinedButtonColors( + contentColor = DebugPanelTheme.colors.content.error, + ), + ) { + Text( + text = stringResource(R.string.konfeature_plugin_reset), + style = DebugPanelTheme.typography.labelLarge, + ) + } +} + +@Composable +private fun CloseButton(onDismissRequest: () -> Unit, modifier: Modifier = Modifier) { + OutlinedButton( + modifier = modifier, + onClick = onDismissRequest, + shape = DebugPanelShapes.medium, + ) { + Text( + text = stringResource(R.string.konfeature_plugin_close), + style = DebugPanelTheme.typography.labelLarge, + ) } } @Composable private fun BooleanEditInput( value: Boolean, - onValueChange: (Any) -> Unit + onValueChange: (Any) -> Unit, + modifier: Modifier = Modifier, ) { var checked by remember { mutableStateOf(value) } + Row( - modifier = Modifier.fillMaxWidth() + modifier = modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, ) { Text( - modifier = Modifier - .weight(1f) - .align(Alignment.CenterVertically), - text = stringResource(id = R.string.konfeature_plugin_edit_dialog_hint_boolean) + text = stringResource(R.string.konfeature_plugin_edit_dialog_hint_boolean), + style = DebugPanelTheme.typography.bodyMedium, + color = DebugPanelTheme.colors.content.primary, + modifier = Modifier.weight(weight = 1f), ) - Checkbox( + PanelToggle( checked = checked, onCheckedChange = { newChecked -> checked = newChecked - onValueChange.invoke(newChecked) - } + onValueChange(newChecked) + }, ) } } @@ -146,22 +205,23 @@ private fun LongEditInput( value: Long, onValueChange: (Any) -> Unit, onEmptyInput: (Boolean) -> Unit, + modifier: Modifier = Modifier, ) { var text by remember { mutableStateOf(value.toString()) } - OutlinedTextField( - modifier = Modifier.fillMaxWidth(), - label = { Text(text = stringResource(id = R.string.konfeature_plugin_edit_dialog_hint_long)) }, + PanelStyledTextField( value = text, onValueChange = { newText -> val newValue = newText.toLongOrNull() if (newValue != null || newText.isEmpty()) { text = newText newValue?.let(onValueChange) - onEmptyInput.invoke(newText.isEmpty()) + onEmptyInput(newText.isEmpty()) } }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number) + label = stringResource(R.string.konfeature_plugin_edit_dialog_hint_long), + modifier = modifier, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), ) } @@ -169,23 +229,24 @@ private fun LongEditInput( private fun DoubleEditInput( value: Double, onValueChange: (Any) -> Unit, - onEmptyImput: (Boolean) -> Unit, + onEmptyInput: (Boolean) -> Unit, + modifier: Modifier = Modifier, ) { var text by remember { mutableStateOf(value.toBigDecimal().toPlainString()) } - OutlinedTextField( - modifier = Modifier.fillMaxWidth(), - label = { Text(text = stringResource(id = R.string.konfeature_plugin_edit_dialog_hint_double)) }, + PanelStyledTextField( value = text, onValueChange = { newText -> val newValue = newText.toDoubleOrNull() if (newValue != null || newText.isEmpty()) { text = newText newValue?.let(onValueChange) - onEmptyImput.invoke(newText.isEmpty()) + onEmptyInput(newText.isEmpty()) } }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal) + label = stringResource(R.string.konfeature_plugin_edit_dialog_hint_double), + modifier = modifier, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal), ) } @@ -193,16 +254,17 @@ private fun DoubleEditInput( private fun StringEditInput( value: String, onValueChange: (Any) -> Unit, + modifier: Modifier = Modifier, ) { var text by remember { mutableStateOf(value) } - OutlinedTextField( - modifier = Modifier.fillMaxWidth(), - label = { Text(text = stringResource(id = R.string.konfeature_plugin_edit_dialog_hint_string)) }, + PanelStyledTextField( value = text, onValueChange = { newText -> text = newText - onValueChange.invoke(newText) + onValueChange(newText) }, + label = stringResource(R.string.konfeature_plugin_edit_dialog_hint_string), + modifier = modifier, ) } diff --git a/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/KonfeatureScreen.kt b/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/KonfeatureScreen.kt index 384ccede..c8a6bc63 100644 --- a/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/KonfeatureScreen.kt +++ b/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/KonfeatureScreen.kt @@ -1,29 +1,29 @@ package com.redmadrobot.debug.plugin.konfeature.ui -import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background +import androidx.compose.foundation.border 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.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material.Button -import androidx.compose.material.Divider -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.OutlinedTextField -import androidx.compose.material.Text -import androidx.compose.material.TextFieldDefaults +import androidx.compose.foundation.lazy.LazyListScope +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -34,9 +34,13 @@ import com.redmadrobot.debug.plugin.konfeature.KonfeaturePluginContainer import com.redmadrobot.debug.plugin.konfeature.R import com.redmadrobot.debug.plugin.konfeature.ui.data.KonfeatureItem import com.redmadrobot.debug.plugin.konfeature.ui.data.KonfeatureViewState -import com.redmadrobot.debug.core.R as CoreR +import com.redmadrobot.debug.uikit.components.PanelSearchBar +import com.redmadrobot.debug.uikit.components.PanelToggle +import com.redmadrobot.debug.uikit.theme.DebugPanelDimensions +import com.redmadrobot.debug.uikit.theme.DebugPanelShapes +import com.redmadrobot.debug.uikit.theme.DebugPanelTheme +import com.redmadrobot.debug.uikit.theme.MonoFontFamily -@OptIn(ExperimentalMaterialApi::class) @Composable internal fun KonfeatureScreen( viewModel: KonfeatureViewModel = provideViewModel { @@ -45,7 +49,7 @@ internal fun KonfeatureScreen( .createKonfeatureViewModel() }, ) { - val state by viewModel.state.collectAsState(KonfeatureViewState()) + val state by viewModel.state.collectAsState() KonfeatureLayout( state = state, @@ -55,6 +59,7 @@ internal fun KonfeatureScreen( onHeaderClick = viewModel::onConfigHeaderClick, onEditClick = viewModel::onEditClick, onSearchQueryChange = viewModel::onSearchQueryChanged, + onBooleanToggle = viewModel::onValueChanged, ) state.editDialogState?.let { dialogState -> @@ -62,12 +67,11 @@ internal fun KonfeatureScreen( state = dialogState, onValueChange = viewModel::onValueChanged, onValueReset = viewModel::onValueReset, - onDismissRequest = viewModel::onEditDialogCloseClicked + onDismissRequest = viewModel::onEditDialogCloseClicked, ) } } -@OptIn(ExperimentalFoundationApi::class) @Composable internal fun KonfeatureLayout( state: KonfeatureViewState, @@ -77,170 +81,330 @@ internal fun KonfeatureLayout( onResetAllClick: () -> Unit, onHeaderClick: (String) -> Unit, onSearchQueryChange: (String) -> Unit, + onBooleanToggle: (String, Boolean) -> Unit, + modifier: Modifier = Modifier, ) { - LazyColumn { - stickyHeader { - KonfeatureHeader( - searchQuery = state.searchQuery, - onSearchQueryChange = onSearchQueryChange, - onRefreshClick = onRefreshClick, - onCollapseAllClick = onCollapseAllClick, - onResetAllClick = onResetAllClick, + Column( + modifier = modifier + .fillMaxSize() + .background(color = DebugPanelTheme.colors.background.primary) + ) { + ToolbarChips( + onRefreshClick = onRefreshClick, + onCollapseAllClick = onCollapseAllClick, + onResetAllClick = onResetAllClick, + ) + PanelSearchBar( + query = state.searchQuery, + onQueryChange = onSearchQueryChange, + placeholder = stringResource(R.string.konfeature_plugin_search_hint), + modifier = Modifier.padding(horizontal = 12.dp), + ) + AnimatedVisibility(visible = state.shouldShowEmptySearchItemsHint) { + Text( + text = stringResource(R.string.konfeature_plugin_search_empty), + style = DebugPanelTheme.typography.bodyMedium, + color = DebugPanelTheme.colors.content.tertiary, + modifier = Modifier.padding(all = 16.dp), ) } + LazyColumn(modifier = Modifier.weight(weight = 1f)) { + konfeatureItems( + state = state, + onHeaderClick = onHeaderClick, + onEditClick = onEditClick, + onBooleanToggle = onBooleanToggle + ) + } + } +} - if (state.shouldShowEmptySearchItemsHint) { - item { - Text( - text = stringResource(R.string.konfeature_plugin_search_empty), - modifier = Modifier.padding(16.dp) +private fun LazyListScope.konfeatureItems( + state: KonfeatureViewState, + onHeaderClick: (String) -> Unit, + onEditClick: (String, Any, Boolean) -> Unit, + onBooleanToggle: (String, Boolean) -> Unit, +) { + items( + items = state.filteredItems, + key = { item -> + when (item) { + is KonfeatureItem.Config -> "config_${item.name}" + is KonfeatureItem.Value -> "value_${item.key}" + } + }, + ) { item -> + when (item) { + is KonfeatureItem.Config -> { + val isCollapsed = !state.isSearchActive && item.name in state.collapsedConfigs + val overrideCount = state.values.count { value -> + value.configName == item.name && value.isDebugSource + } + ConfigGroupHeader( + name = item.description.takeIf { it.isNotEmpty() } ?: item.name, + overrideCount = overrideCount, + isCollapsed = isCollapsed, + onClick = { onHeaderClick(item.name) }, ) } - } - state.filteredItems.forEach { item -> - if (item is KonfeatureItem.Config) { - item(item.name) { - ConfigItem( + is KonfeatureItem.Value -> { + val isVisible = state.isSearchActive || item.configName !in state.collapsedConfigs + if (isVisible) { + ConfigValueItem( item = item, - isCollapsed = !state.isSearchActive && item.name in state.collapsedConfigs, - onHeaderClick = onHeaderClick + onEditClick = onEditClick, + onBooleanToggle = onBooleanToggle, ) } } - - val isExpanded = state.isSearchActive || item is KonfeatureItem.Value && - item.configName !in state.collapsedConfigs - if (item is KonfeatureItem.Value && isExpanded) { - item(item.key) { ValueItem(item = item, onEditClick) } - item { Divider(modifier = Modifier.fillMaxWidth()) } - } } } } @Composable -private fun KonfeatureHeader( - searchQuery: String, - onSearchQueryChange: (String) -> Unit, +private fun ToolbarChips( onRefreshClick: () -> Unit, onCollapseAllClick: () -> Unit, onResetAllClick: () -> Unit, modifier: Modifier = Modifier, ) { - Column( + Row( modifier = modifier - .background(colorResource(id = CoreR.color.super_light_gray)) - .padding(horizontal = 16.dp) + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.spacedBy(space = 8.dp), ) { - KonfeatureSearchBar( - query = searchQuery, - onQueryChange = onSearchQueryChange, - modifier = Modifier.padding(vertical = 8.dp) + ActionChip( + label = stringResource(R.string.konfeature_plugin_refresh), + onClick = onRefreshClick, + ) + ActionChip( + label = stringResource(R.string.konfeature_plugin_collapse_all), + onClick = onCollapseAllClick, + ) + ActionChip( + label = stringResource(R.string.konfeature_plugin_reset_all), + onClick = onResetAllClick, ) - Row { - Button(onClick = onRefreshClick) { - Text(text = stringResource(id = R.string.konfeature_plugin_refresh)) - } - Spacer(modifier = Modifier.weight(1f)) - Button(onClick = onCollapseAllClick) { - Text(text = stringResource(id = R.string.konfeature_plugin_collapse_all)) - } - Spacer(modifier = Modifier.weight(1f)) - Button(onClick = onResetAllClick) { - Text(text = stringResource(id = R.string.konfeature_plugin_reset_all)) - } - } } } @Composable -private fun KonfeatureSearchBar( - query: String, - onQueryChange: (String) -> Unit, - modifier: Modifier = Modifier +private fun ActionChip( + label: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, ) { - OutlinedTextField( - value = query, - onValueChange = onQueryChange, - modifier = modifier.fillMaxWidth(), - placeholder = { - Text(text = stringResource(R.string.konfeature_plugin_search_hint)) - }, - leadingIcon = { - Icon( - painter = painterResource(R.drawable.icon_search), - contentDescription = null + Text( + text = label, + style = DebugPanelTheme.typography.labelLarge, + color = DebugPanelTheme.colors.content.secondary, + modifier = modifier + .clip(shape = DebugPanelShapes.medium) + .border( + width = 1.dp, + color = DebugPanelTheme.colors.stroke.primary, + shape = DebugPanelShapes.medium, ) - }, - trailingIcon = { - if (query.isNotEmpty()) { - IconButton(onClick = { onQueryChange("") }) { - Icon( - painter = painterResource(R.drawable.icon_clear), - contentDescription = stringResource(R.string.konfeature_plugin_search_clear) - ) - } - } - }, - singleLine = true, - colors = TextFieldDefaults.outlinedTextFieldColors( - backgroundColor = Color.White - ) + .clickable(onClick = onClick) + .padding(horizontal = 12.dp, vertical = 4.dp), ) } @Composable -private fun ConfigItem( +private fun ConfigGroupHeader( + name: String, + overrideCount: Int, isCollapsed: Boolean, - item: KonfeatureItem.Config, - onHeaderClick: (String) -> Unit + onClick: () -> Unit, + modifier: Modifier = Modifier, ) { Row( - modifier = Modifier + modifier = modifier .fillMaxWidth() - .clickable { onHeaderClick.invoke(item.name) } - .background(colorResource(id = CoreR.color.super_light_gray)) - .padding(horizontal = 16.dp, vertical = 8.dp) + .clip(shape = DebugPanelShapes.medium) + .clickable(onClick = onClick) + .padding(all = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(space = 4.dp), ) { - Text( - modifier = Modifier.weight(1f), - text = item.description.takeIf { it.isNotEmpty() } ?: item.name - ) - val icon = if (isCollapsed) R.drawable.icon_keyboard_arrow_up else R.drawable.icon_keyboard_arrow_down - Icon( - painter = painterResource(icon), - modifier = Modifier.align(Alignment.CenterVertically), - contentDescription = null + painter = painterResource( + id = if (isCollapsed) { + R.drawable.icon_keyboard_arrow_up + } else { + R.drawable.icon_keyboard_arrow_down + } + ), + contentDescription = null, + tint = DebugPanelTheme.colors.content.tertiary, + modifier = Modifier.size(size = DebugPanelDimensions.iconSizeSmall), + ) + Text( + text = name, + style = DebugPanelTheme.typography.titleMedium, + color = DebugPanelTheme.colors.content.primary, + modifier = Modifier.weight(weight = 1f), ) + if (overrideCount > 0) { + Text( + text = overrideCount.toString(), + style = DebugPanelTheme.typography.labelSmall, + color = DebugPanelTheme.colors.content.accent, + modifier = Modifier + .background( + color = DebugPanelTheme.colors.surface.tertiary, + shape = DebugPanelShapes.small, + ) + .padding(horizontal = 8.dp, vertical = 2.dp), + ) + } } } @Composable -internal fun ValueItem(item: KonfeatureItem.Value, onEditClick: (String, Any, Boolean) -> Unit) { +private fun ConfigValueItem( + item: KonfeatureItem.Value, + onEditClick: (String, Any, Boolean) -> Unit, + onBooleanToggle: (String, Boolean) -> Unit, + modifier: Modifier = Modifier, +) { Row( - modifier = Modifier + modifier = modifier .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 4.dp) + .padding(start = 32.dp, end = 8.dp) + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(space = 8.dp), ) { - Column(Modifier.weight(1f)) { - Text(text = item.description) - Text(text = stringResource(id = R.string.konfeature_plugin_item_key, item.key)) - Text(text = stringResource(id = R.string.konfeature_plugin_item_value, item.value.toString())) - Text( - color = item.sourceColor, - text = stringResource(id = R.string.konfeature_plugin_item_source, item.sourceName) + ValueInfoColumn( + item = item, + modifier = Modifier.weight(weight = 1f), + ) + + when { + item.value is Boolean -> PanelToggle( + checked = item.value, + onCheckedChange = { newValue -> onBooleanToggle(item.key, newValue) }, + ) + + item.editAvailable -> EditButton( + onClick = { onEditClick(item.key, item.value, item.isDebugSource) }, ) } + } +} - if (item.editAvailable) { - IconButton( - modifier = Modifier.align(alignment = Alignment.CenterVertically), - onClick = { onEditClick.invoke(item.key, item.value, item.isDebugSource) } - ) { - Icon(painterResource(R.drawable.icon_edit), contentDescription = null) - } +@Composable +private fun ValueInfoColumn( + item: KonfeatureItem.Value, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Text( + text = item.key, + style = DebugPanelTheme.typography.bodyMedium.copy(fontFamily = MonoFontFamily), + color = DebugPanelTheme.colors.content.secondary, + ) + if (item.description.isNotEmpty()) { + Text( + modifier = Modifier.padding(top = 4.dp), + text = item.description, + style = DebugPanelTheme.typography.bodyMedium.copy(fontFamily = MonoFontFamily), + color = DebugPanelTheme.colors.content.tertiary, + ) + } + if (item.value is Boolean) { + ValueSourceLabel(item = item, modifier = Modifier.padding(top = 8.dp)) + } else { + ValueWithSource(item = item) } } } + +@Composable +private fun ValueWithSource( + item: KonfeatureItem.Value, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier.padding(top = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(space = 8.dp), + ) { + Text( + text = formatValue(value = item.value), + style = DebugPanelTheme.typography.labelMedium, + color = sourceColor(item = item), + ) + ValueSourceLabel(item = item) + } +} + +@Composable +private fun ValueSourceLabel( + item: KonfeatureItem.Value, + modifier: Modifier = Modifier, +) { + when { + item.isDebugSource -> SourceLabel( + source = item.sourceName, + isDebug = true, + modifier = modifier, + ) + + item.sourceName != "Default" -> SourceLabel( + source = item.sourceName, + isDebug = false, + modifier = modifier, + ) + } +} + +@Composable +private fun EditButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + IconButton( + onClick = onClick, + modifier = modifier.size(size = DebugPanelDimensions.iconSizeLarge), + ) { + Icon( + painter = painterResource(R.drawable.icon_edit), + contentDescription = null, + tint = DebugPanelTheme.colors.content.accent, + modifier = Modifier.size(size = DebugPanelDimensions.iconSizeSmall), + ) + } +} + +@Composable +private fun SourceLabel( + source: String, + isDebug: Boolean, + modifier: Modifier = Modifier, +) { + Text( + text = source, + style = DebugPanelTheme.typography.labelMedium, + color = if (isDebug) { + DebugPanelTheme.colors.content.teal + } else { + DebugPanelTheme.colors.source.remoteText + }, + modifier = modifier, + ) +} + +private fun formatValue(value: Any): String { + return if (value is String) "\"$value\"" else value.toString() +} + +@Composable +private fun sourceColor(item: KonfeatureItem.Value): Color = when { + item.isDebugSource -> DebugPanelTheme.colors.content.teal + item.sourceName != "Default" -> DebugPanelTheme.colors.source.remoteText + else -> DebugPanelTheme.colors.content.tertiary +} diff --git a/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/KonfeatureViewModel.kt b/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/KonfeatureViewModel.kt index 49abcdd6..ec720919 100644 --- a/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/KonfeatureViewModel.kt +++ b/plugins/plugin-konfeature/src/main/kotlin/com/redmadrobot/debug/plugin/konfeature/ui/KonfeatureViewModel.kt @@ -13,8 +13,8 @@ import com.redmadrobot.konfeature.Konfeature import com.redmadrobot.konfeature.source.FeatureValueSource import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.launchIn @@ -32,7 +32,7 @@ internal class KonfeatureViewModel( private val _state = MutableStateFlow(KonfeatureViewState()) private val _searchQueryFlow = MutableStateFlow("") - val state: Flow = _state.asStateFlow() + val state: StateFlow = _state.asStateFlow() init { observeKonfeatureValues() diff --git a/plugins/plugin-servers/src/main/kotlin/com/redmadrobot/debug/plugin/servers/ui/ServersScreen.kt b/plugins/plugin-servers/src/main/kotlin/com/redmadrobot/debug/plugin/servers/ui/ServersScreen.kt index 33afda59..d5b425f7 100644 --- a/plugins/plugin-servers/src/main/kotlin/com/redmadrobot/debug/plugin/servers/ui/ServersScreen.kt +++ b/plugins/plugin-servers/src/main/kotlin/com/redmadrobot/debug/plugin/servers/ui/ServersScreen.kt @@ -19,8 +19,6 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState @@ -44,6 +42,7 @@ import com.redmadrobot.debug.plugin.servers.ServersPlugin import com.redmadrobot.debug.plugin.servers.ServersPluginContainer import com.redmadrobot.debug.plugin.servers.data.model.DebugServer import com.redmadrobot.debug.uikit.components.PanelBottomSheet +import com.redmadrobot.debug.uikit.components.PanelStyledTextField import com.redmadrobot.debug.uikit.theme.DebugPanelDimensions import com.redmadrobot.debug.uikit.theme.DebugPanelShapes import com.redmadrobot.debug.uikit.theme.DebugPanelTheme @@ -273,7 +272,7 @@ private fun ServerBottomSheet( title = title, onDismiss = onDismiss, ) { - ServerTextField( + PanelStyledTextField( value = state.serverName, onValueChange = onNameChange, label = stringResource(R.string.name), @@ -281,7 +280,7 @@ private fun ServerBottomSheet( errorMessage = state.inputErrors?.nameError?.let { stringResource(it) }, ) Spacer(modifier = Modifier.height(16.dp)) - ServerTextField( + PanelStyledTextField( value = state.serverUrl, onValueChange = onUrlChange, label = stringResource(R.string.server_host_hint), @@ -302,43 +301,3 @@ private fun ServerBottomSheet( Spacer(modifier = Modifier.height(24.dp)) } } - -@Composable -private fun ServerTextField( - value: String, - label: String, - onValueChange: (String) -> Unit, - modifier: Modifier = Modifier, - isError: Boolean = false, - errorMessage: String? = null, -) { - Column(modifier = modifier.fillMaxWidth()) { - OutlinedTextField( - value = value, - onValueChange = onValueChange, - modifier = Modifier.fillMaxWidth(), - label = { Text(label, style = DebugPanelTheme.typography.bodyMedium) }, - isError = isError, - singleLine = true, - textStyle = DebugPanelTheme.typography.bodyMedium.copy( - color = DebugPanelTheme.colors.content.primary, - ), - colors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = DebugPanelTheme.colors.content.accent, - unfocusedBorderColor = DebugPanelTheme.colors.stroke.secondary, - focusedLabelColor = DebugPanelTheme.colors.content.accent, - unfocusedLabelColor = DebugPanelTheme.colors.content.tertiary, - errorBorderColor = DebugPanelTheme.colors.content.error, - cursorColor = DebugPanelTheme.colors.content.accent, - ), - ) - if (isError && errorMessage != null) { - Text( - text = errorMessage, - style = DebugPanelTheme.typography.bodySmall, - color = DebugPanelTheme.colors.content.error, - modifier = Modifier.padding(top = 4.dp), - ) - } - } -}