diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/components/ChipGroup.kt b/app/src/main/kotlin/cn/super12138/todo/ui/components/ChipGroup.kt index 70531b1..ab5bffb 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/components/ChipGroup.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/components/ChipGroup.kt @@ -43,9 +43,6 @@ fun FilterChipGroup( defaultSelectedItemIndex: Int = 0, onSelectedChanged: (Int) -> Unit = {} ) { - SideEffect { - Log.d("TAG", "来自 FilterChipGroup:重组啦") - } val view = LocalView.current var selectedItemIndex by rememberSaveable { mutableIntStateOf(defaultSelectedItemIndex) } diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/components/Dialogs.kt b/app/src/main/kotlin/cn/super12138/todo/ui/components/Dialogs.kt index 3263b45..baf89df 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/components/Dialogs.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/components/Dialogs.kt @@ -2,24 +2,15 @@ package cn.super12138.todo.ui.components import androidx.compose.foundation.layout.Column 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.material.icons.Icons import androidx.compose.material.icons.outlined.ErrorOutline -import androidx.compose.material.icons.outlined.Info import androidx.compose.material3.AlertDialog import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton 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.platform.LocalView @@ -28,66 +19,6 @@ import androidx.compose.ui.window.DialogProperties import cn.super12138.todo.R 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 fun ConfirmDialog( modifier: Modifier = Modifier, diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/TodoEditorPage.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/TodoEditorPage.kt index 7cca560..43f06d7 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/TodoEditorPage.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/TodoEditorPage.kt @@ -1,6 +1,5 @@ package cn.super12138.todo.ui.pages.editor -import android.util.Log import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibilityScope @@ -33,7 +32,6 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment 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.ChipItem 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.pages.editor.components.TodoCategoryChip 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.TodoPrioritySlider @@ -67,6 +65,7 @@ fun TodoEditorPage( sharedTransitionScope: SharedTransitionScope, animatedVisibilityScope: AnimatedVisibilityScope ) { + // TODO: 本页及其相关组件重组性能检查优化 val view = LocalView.current val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() @@ -83,6 +82,7 @@ fun TodoEditorPage( var defaultIndex by remember { mutableIntStateOf(0) } LaunchedEffect(originalCategories, toDo) { + if (originalCategories.isEmpty()) return@LaunchedEffect if (toDo == null) { val index = if (categories.size == 1) -1 else 0 defaultIndex = index @@ -91,9 +91,7 @@ fun TodoEditorPage( val index = categories.firstOrNull { it.name == toDo.category }?.id ?: -1 defaultIndex = index uiState.selectedCategoryIndex = index - if (index == -1) { - uiState.categoryContent = toDo.category - } + if (index != -1) uiState.categoryContent = "" } } @@ -174,8 +172,7 @@ fun TodoEditorPage( value = uiState.toDoContent, onValueChange = { uiState.toDoContent = it }, isError = uiState.isErrorContent, - modifier = Modifier - .fillMaxWidth() + modifier = Modifier.fillMaxWidth() ) } } @@ -186,10 +183,11 @@ fun TodoEditorPage( style = MaterialTheme.typography.titleMedium ) - FilterChipGroup( + TodoCategoryChip( items = categories, defaultSelectedItemIndex = defaultIndex, - onSelectedChanged = { uiState.selectedCategoryIndex = it }, + isLoading = originalCategories.isEmpty(), + onCategorySelected = { uiState.selectedCategoryIndex = it }, modifier = Modifier.fillMaxWidth() ) diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoCategoryChip.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoCategoryChip.kt new file mode 100644 index 0000000..c4c5707 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoCategoryChip.kt @@ -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, + 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 + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsDataCategory.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsDataCategory.kt index 99c0468..58e7734 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsDataCategory.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsDataCategory.kt @@ -28,9 +28,9 @@ import androidx.compose.ui.res.stringResource import cn.super12138.todo.R import cn.super12138.todo.logic.datastore.DataStoreManager 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.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 @OptIn(ExperimentalMaterial3Api::class) @@ -39,6 +39,7 @@ fun SettingsDataCategory( onNavigateUp: () -> Unit, modifier: Modifier = Modifier ) { + // TODO: 本页及其相关组件重组性能检查优化 val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() val snackbarHostState = remember { SnackbarHostState() } val scope = rememberCoroutineScope() @@ -77,13 +78,21 @@ fun SettingsDataCategory( } } else { 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, text = stringResource(R.string.tip_enter_category), onSave = { diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/category/CategoryDialog.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/category/CategoryDialog.kt new file mode 100644 index 0000000..671f28d --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/category/CategoryDialog.kt @@ -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 + ) +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/category/CategoryItem.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/category/CategoryItem.kt new file mode 100644 index 0000000..1367653 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/components/category/CategoryItem.kt @@ -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) + ) + } + } +} \ No newline at end of file