feat: 优化编辑器逻辑及代码 & 优化添加类别对话框样式

This commit is contained in:
Super12138 2025-07-03 21:54:44 +08:00
parent 29d71fc060
commit 75913080ea
7 changed files with 226 additions and 86 deletions

View file

@ -43,9 +43,6 @@ fun FilterChipGroup(
defaultSelectedItemIndex: Int = 0, defaultSelectedItemIndex: Int = 0,
onSelectedChanged: (Int) -> Unit = {} onSelectedChanged: (Int) -> Unit = {}
) { ) {
SideEffect {
Log.d("TAG", "来自 FilterChipGroup重组啦")
}
val view = LocalView.current val view = LocalView.current
var selectedItemIndex by rememberSaveable { mutableIntStateOf(defaultSelectedItemIndex) } var selectedItemIndex by rememberSaveable { mutableIntStateOf(defaultSelectedItemIndex) }

View file

@ -2,24 +2,15 @@ package cn.super12138.todo.ui.components
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ErrorOutline import androidx.compose.material.icons.outlined.ErrorOutline
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
@ -28,66 +19,6 @@ import androidx.compose.ui.window.DialogProperties
import cn.super12138.todo.R import cn.super12138.todo.R
import cn.super12138.todo.utils.VibrationUtils import cn.super12138.todo.utils.VibrationUtils
@Composable
fun FiveCharPromptDialog(
modifier: Modifier = Modifier,
visible: Boolean,
icon: ImageVector = Icons.Outlined.Info,
title: String = stringResource(R.string.tip_tips),
text: String,
confirmButtonText: String = stringResource(R.string.action_save),
showDismissButton: Boolean = true,
dismissButtonText: String = stringResource(R.string.action_cancel),
properties: DialogProperties = DialogProperties(),
onSave: (String) -> Unit,
onDismiss: () -> Unit
) {
val textFieldState = rememberTextFieldState()
var isError by rememberSaveable { mutableStateOf(false) }
val supportingText = listOf(
stringResource(R.string.tip_max_length_5),
stringResource(R.string.error_no_content_entered),
stringResource(R.string.error_exceeds_5_chars)
)
var currentSupportingText by remember { mutableStateOf(supportingText[0]) }
BasicDialog(
visible = visible,
icon = icon,
title = title,
text = { // 已经是实现好滚动的Column布局
Text(text)
OutlinedTextField(
state = textFieldState,
lineLimits = TextFieldLineLimits.SingleLine,
label = { Text(stringResource(R.string.label_enter_sth)) },
supportingText = { Text(currentSupportingText) },
isError = isError
)
},
confirmButton = confirmButtonText,
dismissButton = if (showDismissButton) dismissButtonText else null,
onConfirm = {
if (textFieldState.text.trim().isEmpty()) {
isError = true
currentSupportingText = supportingText[1]
return@BasicDialog
} else if (textFieldState.text.trim().length > 5) {
isError = true
currentSupportingText = supportingText[2]
} else {
onSave(textFieldState.text.toString())
onDismiss()
}
},
onDismiss = onDismiss,
properties = properties,
modifier = modifier
)
}
@Composable @Composable
fun ConfirmDialog( fun ConfirmDialog(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,

View file

@ -1,6 +1,5 @@
package cn.super12138.todo.ui.pages.editor package cn.super12138.todo.ui.pages.editor
import android.util.Log
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.AnimatedVisibilityScope
@ -33,7 +32,6 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -48,8 +46,8 @@ import cn.super12138.todo.ui.TodoDefaults
import cn.super12138.todo.ui.components.AnimatedExtendedFloatingActionButton import cn.super12138.todo.ui.components.AnimatedExtendedFloatingActionButton
import cn.super12138.todo.ui.components.ChipItem import cn.super12138.todo.ui.components.ChipItem
import cn.super12138.todo.ui.components.ConfirmDialog import cn.super12138.todo.ui.components.ConfirmDialog
import cn.super12138.todo.ui.components.FilterChipGroup
import cn.super12138.todo.ui.components.LargeTopAppBarScaffold import cn.super12138.todo.ui.components.LargeTopAppBarScaffold
import cn.super12138.todo.ui.pages.editor.components.TodoCategoryChip
import cn.super12138.todo.ui.pages.editor.components.TodoCategoryTextField import cn.super12138.todo.ui.pages.editor.components.TodoCategoryTextField
import cn.super12138.todo.ui.pages.editor.components.TodoContentTextField import cn.super12138.todo.ui.pages.editor.components.TodoContentTextField
import cn.super12138.todo.ui.pages.editor.components.TodoPrioritySlider import cn.super12138.todo.ui.pages.editor.components.TodoPrioritySlider
@ -67,6 +65,7 @@ fun TodoEditorPage(
sharedTransitionScope: SharedTransitionScope, sharedTransitionScope: SharedTransitionScope,
animatedVisibilityScope: AnimatedVisibilityScope animatedVisibilityScope: AnimatedVisibilityScope
) { ) {
// TODO: 本页及其相关组件重组性能检查优化
val view = LocalView.current val view = LocalView.current
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
@ -83,6 +82,7 @@ fun TodoEditorPage(
var defaultIndex by remember { mutableIntStateOf(0) } var defaultIndex by remember { mutableIntStateOf(0) }
LaunchedEffect(originalCategories, toDo) { LaunchedEffect(originalCategories, toDo) {
if (originalCategories.isEmpty()) return@LaunchedEffect
if (toDo == null) { if (toDo == null) {
val index = if (categories.size == 1) -1 else 0 val index = if (categories.size == 1) -1 else 0
defaultIndex = index defaultIndex = index
@ -91,9 +91,7 @@ fun TodoEditorPage(
val index = categories.firstOrNull { it.name == toDo.category }?.id ?: -1 val index = categories.firstOrNull { it.name == toDo.category }?.id ?: -1
defaultIndex = index defaultIndex = index
uiState.selectedCategoryIndex = index uiState.selectedCategoryIndex = index
if (index == -1) { if (index != -1) uiState.categoryContent = ""
uiState.categoryContent = toDo.category
}
} }
} }
@ -174,8 +172,7 @@ fun TodoEditorPage(
value = uiState.toDoContent, value = uiState.toDoContent,
onValueChange = { uiState.toDoContent = it }, onValueChange = { uiState.toDoContent = it },
isError = uiState.isErrorContent, isError = uiState.isErrorContent,
modifier = Modifier modifier = Modifier.fillMaxWidth()
.fillMaxWidth()
) )
} }
} }
@ -186,10 +183,11 @@ fun TodoEditorPage(
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium
) )
FilterChipGroup( TodoCategoryChip(
items = categories, items = categories,
defaultSelectedItemIndex = defaultIndex, defaultSelectedItemIndex = defaultIndex,
onSelectedChanged = { uiState.selectedCategoryIndex = it }, isLoading = originalCategories.isEmpty(),
onCategorySelected = { uiState.selectedCategoryIndex = it },
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )

View file

@ -0,0 +1,41 @@
package cn.super12138.todo.ui.pages.editor.components
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import cn.super12138.todo.ui.components.ChipItem
import cn.super12138.todo.ui.components.FilterChipGroup
@Composable
fun TodoCategoryChip(
modifier: Modifier = Modifier,
items: List<ChipItem>,
defaultSelectedItemIndex: Int,
isLoading: Boolean = false,
onCategorySelected: (Int) -> Unit
) {
Box(modifier = modifier.fillMaxWidth()) {
AnimatedContent(
targetState = isLoading,
transitionSpec = { fadeIn(tween(100)) togetherWith fadeOut(tween(100)) }
) {
if (it) {
CircularProgressIndicator()
} else {
FilterChipGroup(
modifier = Modifier,
items = items,
defaultSelectedItemIndex = defaultSelectedItemIndex,
onSelectedChanged = onCategorySelected
)
}
}
}
}

View file

@ -28,9 +28,9 @@ import androidx.compose.ui.res.stringResource
import cn.super12138.todo.R import cn.super12138.todo.R
import cn.super12138.todo.logic.datastore.DataStoreManager import cn.super12138.todo.logic.datastore.DataStoreManager
import cn.super12138.todo.ui.components.AnimatedExtendedFloatingActionButton import cn.super12138.todo.ui.components.AnimatedExtendedFloatingActionButton
import cn.super12138.todo.ui.components.FiveCharPromptDialog
import cn.super12138.todo.ui.components.LargeTopAppBarScaffold import cn.super12138.todo.ui.components.LargeTopAppBarScaffold
import cn.super12138.todo.ui.pages.settings.components.SettingsCategory import cn.super12138.todo.ui.pages.settings.components.category.CategoryItem
import cn.super12138.todo.ui.pages.settings.components.category.CategoryPromptDialog
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@ -39,6 +39,7 @@ fun SettingsDataCategory(
onNavigateUp: () -> Unit, onNavigateUp: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
// TODO: 本页及其相关组件重组性能检查优化
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
val snackbarHostState = remember { SnackbarHostState() } val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@ -77,13 +78,21 @@ fun SettingsDataCategory(
} }
} else { } else {
items(items = categories, key = { it }) { items(items = categories, key = { it }) {
SettingsCategory(it) CategoryItem(
name = it,
onDelete = { it ->
scope.launch {
DataStoreManager.setCategories(categories - it)
}
},
modifier = Modifier.animateItem()
)
} }
} }
} }
} }
FiveCharPromptDialog( CategoryPromptDialog(
visible = showDialog, visible = showDialog,
text = stringResource(R.string.tip_enter_category), text = stringResource(R.string.tip_enter_category),
onSave = { onSave = {

View file

@ -0,0 +1,98 @@
package cn.super12138.todo.ui.pages.settings.components.category
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.input.TextFieldLineLimits
import androidx.compose.foundation.text.input.clearText
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import cn.super12138.todo.R
import cn.super12138.todo.ui.components.BasicDialog
@Composable
fun CategoryPromptDialog(
modifier: Modifier = Modifier,
visible: Boolean,
icon: ImageVector = Icons.Outlined.Info,
title: String = stringResource(R.string.tip_tips),
text: String,
confirmButtonText: String = stringResource(R.string.action_save),
showDismissButton: Boolean = true,
dismissButtonText: String = stringResource(R.string.action_cancel),
properties: DialogProperties = DialogProperties(),
onSave: (String) -> Unit,
onDismiss: () -> Unit
) {
val textFieldState = rememberTextFieldState()
var isError by rememberSaveable { mutableStateOf(false) }
val supportingText = listOf(
stringResource(R.string.tip_max_length_5),
stringResource(R.string.error_no_content_entered),
stringResource(R.string.error_exceeds_5_chars)
)
var currentSupportingText by remember { mutableStateOf(supportingText[0]) }
BasicDialog(
visible = visible,
icon = icon,
title = title,
text = {
// 已经是实现好滚动的Column布局
Text(text)
Spacer(Modifier.size(3.dp))
OutlinedTextField(
state = textFieldState,
lineLimits = TextFieldLineLimits.SingleLine,
label = { Text(stringResource(R.string.label_enter_sth)) },
supportingText = { AnimatedContent(targetState = currentSupportingText) { Text(it) } },
isError = isError
)
},
confirmButton = confirmButtonText,
dismissButton = if (showDismissButton) dismissButtonText else null,
onConfirm = {
val trimmedText = textFieldState.text.trim()
when {
trimmedText.isEmpty() -> {
isError = true
currentSupportingText = supportingText[1]
return@BasicDialog
}
trimmedText.length > 5 -> {
isError = true
currentSupportingText = supportingText[2]
return@BasicDialog
}
else -> {
onSave(trimmedText.toString())
isError = false
currentSupportingText = supportingText[0]
textFieldState.clearText()
onDismiss()
}
}
},
onDismiss = onDismiss,
properties = properties,
modifier = modifier
)
}

View file

@ -0,0 +1,66 @@
package cn.super12138.todo.ui.pages.settings.components.category
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.sp
import cn.super12138.todo.R
import cn.super12138.todo.ui.TodoDefaults
import cn.super12138.todo.utils.VibrationUtils
@Composable
fun CategoryItem(
modifier: Modifier = Modifier,
name: String,
onDelete: (String) -> Unit = {}
) {
val view = LocalView.current
Row(
modifier = modifier
.fillMaxWidth()
.wrapContentHeight()
.clip(MaterialTheme.shapes.large)
.padding(
horizontal = TodoDefaults.settingsItemHorizontalPadding,
vertical = TodoDefaults.settingsItemVerticalPadding
),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = name,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.titleLarge.copy(
color = MaterialTheme.colorScheme.onSurface,
fontSize = 20.sp
),
modifier = Modifier.weight(1f)
)
IconButton(
onClick = {
VibrationUtils.performHapticFeedback(view)
onDelete(name)
}
) {
Icon(
imageVector = Icons.Outlined.Delete,
contentDescription = stringResource(R.string.action_delete)
)
}
}
}