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 6347f87..f8b7524 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 @@ -34,10 +34,10 @@ import cn.super12138.todo.utils.VibrationUtils @OptIn(ExperimentalLayoutApi::class) @Composable fun FilterChipGroup( + modifier: Modifier = Modifier, items: List, defaultSelectedItemIndex: Int = 0, - onSelectedChanged: (Int) -> Unit = {}, - modifier: Modifier = Modifier + onSelectedChanged: (Int) -> Unit = {} ) { val view = LocalView.current var selectedItemId by rememberSaveable { mutableIntStateOf(defaultSelectedItemIndex) } @@ -80,10 +80,8 @@ private fun FilterChipItem( ) } }, - label = { - Text(text) - }, - modifier = Modifier.padding(end = 10.dp) + label = { Text(text) }, + modifier = modifier.padding(end = 10.dp) ) } 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 de24e06..db439df 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 @@ -5,17 +5,17 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionScope +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically 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.imePadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.Undo import androidx.compose.material.icons.outlined.Delete @@ -24,17 +24,11 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch import androidx.compose.material3.Text -import androidx.compose.material3.TextField import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.mutableIntStateOf -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.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -53,6 +47,8 @@ import cn.super12138.todo.ui.components.FilterChipGroup import cn.super12138.todo.ui.components.LargeTopAppBarScaffold 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.TodoSubjectTextField +import cn.super12138.todo.ui.pages.editor.state.rememberEditorState import cn.super12138.todo.utils.VibrationUtils @OptIn(ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class) @@ -66,34 +62,16 @@ fun TodoEditorPage( sharedTransitionScope: SharedTransitionScope, animatedVisibilityScope: AnimatedVisibilityScope ) { - var showExitConfirmDialog by rememberSaveable { mutableStateOf(false) } - var showDeleteConfirmDialog by rememberSaveable { mutableStateOf(false) } - val view = LocalView.current val context = LocalContext.current val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() - var toDoContent by rememberSaveable { mutableStateOf(toDo?.content ?: "") } - var isErrorContent by rememberSaveable { mutableStateOf(false) } - var selectedSubjectId by rememberSaveable { mutableIntStateOf(toDo?.subject ?: 0) } - var subjectContent by rememberSaveable { mutableStateOf(toDo?.customSubject ?: "") } - var isErrorSubject by rememberSaveable { mutableStateOf(false) } - var priorityState by rememberSaveable { mutableFloatStateOf(toDo?.priority ?: 0f) } - var completedSwitchState by rememberSaveable { mutableStateOf(toDo?.isCompleted == true) } - - val isCustomSubject by remember { - derivedStateOf { selectedSubjectId == Subjects.Custom.id } - } + val uiState = rememberEditorState(initialTodo = toDo) + val isCustomSubject by remember { derivedStateOf { uiState.selectedSubjectId == Subjects.Custom.id } } fun checkModifiedBeforeBack() { - var isModified = false - if ((toDo?.content ?: "") != toDoContent) isModified = true - if ((toDo?.subject ?: 0) != selectedSubjectId) isModified = true - if ((toDo?.customSubject ?: "") != subjectContent) isModified = true - if ((toDo?.priority ?: 0f) != priorityState) isModified = true - if ((toDo?.isCompleted == true) != completedSwitchState) isModified = true - if (isModified) { - showExitConfirmDialog = true + if (uiState.isModified()) { + uiState.showExitConfirmDialog = true } else { onNavigateUp() } @@ -115,7 +93,7 @@ fun TodoEditorPage( text = stringResource(R.string.action_delete), expanded = true, containerColor = MaterialTheme.colorScheme.errorContainer, - onClick = { showDeleteConfirmDialog = true }, + onClick = { uiState.showDeleteConfirmDialog = true }, modifier = Modifier.imePadding() ) } @@ -124,23 +102,12 @@ fun TodoEditorPage( text = stringResource(R.string.action_save), expanded = true, onClick = { - isErrorContent = toDoContent.trim().isEmpty() - isErrorSubject = subjectContent.trim() - .isEmpty() && selectedSubjectId == Subjects.Custom.id - if (isErrorContent || isErrorSubject) return@AnimatedExtendedFloatingActionButton - - isErrorContent = false - isErrorSubject = false - onSave( - TodoEntity( - content = toDoContent, - subject = selectedSubjectId, - customSubject = subjectContent, - isCompleted = completedSwitchState, - priority = priorityState, - id = toDo?.id ?: 0 - ) - ) + if (uiState.setErrorIfNotValid()) { + return@AnimatedExtendedFloatingActionButton + } else { + uiState.clearError() + onSave(uiState.getEntity()) + } }, modifier = Modifier .imePadding() @@ -155,138 +122,129 @@ fun TodoEditorPage( onBack = { checkModifiedBeforeBack() }, modifier = modifier ) { innerPadding -> - Column( + LazyColumn( + verticalArrangement = Arrangement.spacedBy(5.dp), modifier = Modifier .padding(innerPadding) .padding(horizontal = TodoDefaults.screenPadding) .fillMaxSize() - .verticalScroll(rememberScrollState()) ) { - with(sharedTransitionScope) { - TodoContentTextField( - value = toDoContent, - onValueChange = { toDoContent = it }, - isError = isErrorContent, - modifier = Modifier - .fillMaxWidth() - .sharedBounds( - sharedContentState = rememberSharedContentState("${Constants.KEY_TODO_CONTENT_TRANSITION}_${toDo?.id}"), - animatedVisibilityScope = animatedVisibilityScope - ) - ) - } - - Spacer(Modifier.size(5.dp)) - - Text( - text = stringResource(R.string.label_subject), - style = MaterialTheme.typography.titleMedium - ) - - Spacer(Modifier.size(5.dp)) - - val subjects = remember { - Subjects.entries.map { - ChipItem( - id = it.id, - text = it.getDisplayName(context) - ) - } - } - FilterChipGroup( - items = subjects, - defaultSelectedItemIndex = toDo?.subject ?: Subjects.Chinese.id, - onSelectedChanged = { - selectedSubjectId = it - }, - modifier = Modifier.fillMaxWidth() - ) - AnimatedVisibility(isCustomSubject) { + item { with(sharedTransitionScope) { - TextField( - value = subjectContent, - onValueChange = { subjectContent = it }, - label = { Text(stringResource(R.string.label_enter_subject_name)) }, - isError = isErrorSubject, - supportingText = { - AnimatedVisibility(isErrorSubject) { - Text(stringResource(R.string.error_no_content_entered)) - } - }, + TodoContentTextField( + value = uiState.toDoContent, + onValueChange = { uiState.toDoContent = it }, + isError = uiState.isErrorContent, modifier = Modifier .fillMaxWidth() .sharedBounds( - sharedContentState = rememberSharedContentState("${Constants.KEY_TODO_SUBJECT_TRANSITION}_${toDo?.id}"), + sharedContentState = rememberSharedContentState("${Constants.KEY_TODO_CONTENT_TRANSITION}_${toDo?.id}"), animatedVisibilityScope = animatedVisibilityScope ) - .padding(top = 5.dp) ) } } - Spacer(Modifier.size(10.dp)) - - Text( - text = stringResource(R.string.label_priority), - style = MaterialTheme.typography.titleMedium - ) - - Spacer(Modifier.size(5.dp)) - - TodoPrioritySlider( - value = priorityState, - onValueChange = { priorityState = it }, - ) - - Spacer(Modifier.size(10.dp)) - - if (toDo != null) { + item { Text( - text = stringResource(R.string.label_more), + text = stringResource(R.string.label_subject), style = MaterialTheme.typography.titleMedium ) - Spacer(Modifier.size(5.dp)) - - Row( - verticalAlignment = Alignment.CenterVertically, + val subjects = remember { + Subjects.entries.map { + ChipItem( + id = it.id, + text = it.getDisplayName(context) + ) + } + } + FilterChipGroup( + items = subjects, + defaultSelectedItemIndex = toDo?.subject ?: Subjects.Chinese.id, + onSelectedChanged = { uiState.selectedSubjectId = it }, modifier = Modifier.fillMaxWidth() + ) + AnimatedVisibility( + visible = isCustomSubject, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() ) { - Text( - text = stringResource(R.string.tip_mark_completed), - style = MaterialTheme.typography.labelLarge, - modifier = Modifier.padding(end = 10.dp) - ) - Switch( - checked = completedSwitchState, - onCheckedChange = { - completedSwitchState = it - VibrationUtils.performHapticFeedback(view) - } - ) + with(sharedTransitionScope) { + TodoSubjectTextField( + value = uiState.subjectContent, + onValueChange = { uiState.subjectContent = it }, + isError = uiState.isErrorSubject, + modifier = Modifier + .fillMaxWidth() + .sharedBounds( + sharedContentState = rememberSharedContentState("${Constants.KEY_TODO_SUBJECT_TRANSITION}_${toDo?.id}"), + animatedVisibilityScope = animatedVisibilityScope + ) + ) + } } } - Spacer(Modifier.size(20.dp)) + item { + Text( + text = stringResource(R.string.label_priority), + style = MaterialTheme.typography.titleMedium + ) + + TodoPrioritySlider( + value = { uiState.priorityState }, + onValueChange = { uiState.priorityState = it }, + ) + } + + item { + if (toDo != null) { + Text( + text = stringResource(R.string.label_more), + style = MaterialTheme.typography.titleMedium + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = stringResource(R.string.tip_mark_completed), + style = MaterialTheme.typography.labelLarge, + modifier = Modifier.padding(end = 10.dp) + ) + Switch( + checked = uiState.isCompleted, + onCheckedChange = { + VibrationUtils.performHapticFeedback(view) + uiState.isCompleted = it + } + ) + } + } + } + + //Spacer(Modifier.size(20.dp)) } } ConfirmDialog( - visible = showExitConfirmDialog, + visible = uiState.showExitConfirmDialog, icon = Icons.AutoMirrored.Outlined.Undo, text = stringResource(R.string.tip_discard_changes), onConfirm = { - showExitConfirmDialog = false + uiState.showExitConfirmDialog = false onNavigateUp() }, - onDismiss = { showExitConfirmDialog = false } + onDismiss = { uiState.showExitConfirmDialog = false } ) ConfirmDialog( - visible = showDeleteConfirmDialog, + visible = uiState.showDeleteConfirmDialog, icon = Icons.Outlined.Delete, text = stringResource(R.string.tip_delete_task, 1), onConfirm = onDelete, - onDismiss = { showDeleteConfirmDialog = false } + onDismiss = { uiState.showDeleteConfirmDialog = false } ) } \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoContentTextField.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoEditorTextFields.kt similarity index 61% rename from app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoContentTextField.kt rename to app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoEditorTextFields.kt index 5078951..44dd0a5 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoContentTextField.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoEditorTextFields.kt @@ -27,4 +27,25 @@ fun TodoContentTextField( }, modifier = modifier ) +} + +@Composable +fun TodoSubjectTextField( + value: String, + onValueChange: (String) -> Unit, + isError: Boolean, + modifier: Modifier = Modifier +) { + TextField( + value = value, + onValueChange = onValueChange, + label = { Text(stringResource(R.string.label_enter_subject_name)) }, + isError = isError, + supportingText = { + AnimatedVisibility(isError) { + Text(stringResource(R.string.error_no_content_entered)) + } + }, + modifier = modifier + ) } \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoPrioritySlider.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoPrioritySlider.kt index 51513a2..4b416bc 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoPrioritySlider.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoPrioritySlider.kt @@ -28,7 +28,7 @@ import cn.super12138.todo.utils.VibrationUtils @OptIn(ExperimentalMaterial3Api::class) @Composable fun TodoPrioritySlider( - value: Float, + value: () -> Float, onValueChange: (Float) -> Unit, modifier: Modifier = Modifier ) { @@ -39,7 +39,7 @@ fun TodoPrioritySlider( val interactionSource = remember { MutableInteractionSource() } Slider( - value = value, + value = value(), onValueChange = { VibrationUtils.performHapticFeedback(view, HapticFeedbackConstants.LONG_PRESS) onValueChange(it) @@ -55,7 +55,7 @@ fun TodoPrioritySlider( .sizeIn(45.dp, 25.dp) .wrapContentWidth() ) { - Text(priorityName[Priority.fromFloat(value).ordinal]) + Text(priorityName[Priority.fromFloat(value()).ordinal]) } }, interactionSource = interactionSource @@ -66,9 +66,9 @@ fun TodoPrioritySlider( modifier = modifier.semantics { contentDescription = context.getString(R.string.label_priority) + priorityName[Priority.fromFloat( - value + value() ).ordinal] - stateDescription = priorityName[Priority.fromFloat(value).ordinal] + stateDescription = priorityName[Priority.fromFloat(value()).ordinal] liveRegion = LiveRegionMode.Polite } ) diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/state/EditorState.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/state/EditorState.kt new file mode 100644 index 0000000..f652190 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/state/EditorState.kt @@ -0,0 +1,113 @@ +package cn.super12138.todo.ui.pages.editor.state + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.SaverScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import cn.super12138.todo.logic.database.TodoEntity +import cn.super12138.todo.logic.model.Subjects + +class EditorState( + val initialTodo: TodoEntity? = null, +) { + var toDoContent by mutableStateOf(initialTodo?.content ?: "") + var isErrorContent by mutableStateOf(false) + var selectedSubjectId by mutableIntStateOf(initialTodo?.subject ?: 0) + var subjectContent by mutableStateOf(initialTodo?.customSubject ?: "") + var isErrorSubject by mutableStateOf(false) + var priorityState by mutableFloatStateOf(initialTodo?.priority ?: 0f) + var isCompleted by mutableStateOf(initialTodo?.isCompleted == true) + var showExitConfirmDialog by mutableStateOf(false) + var showDeleteConfirmDialog by mutableStateOf(false) + + /** + * 检查待办内容和学科的一者是否无有效,如果无效则为文本框设置错误状态 + * + * @return 它们二者是否都有效。有一者无效就返回 true + */ + fun setErrorIfNotValid(): Boolean { + isErrorContent = toDoContent.trim().isEmpty() + isErrorSubject = subjectContent.trim().isEmpty() && + selectedSubjectId == Subjects.Custom.id + return isErrorContent && isErrorSubject + } + + /** + * 清除文本框全部错误 + */ + fun clearError() { + isErrorContent = false + isErrorSubject = false + } + + /** + * 获取编辑后的待办实体 + * + * @return TodoEntity 待办实体 + */ + fun getEntity(): TodoEntity = TodoEntity( + id = initialTodo?.id ?: 0, + content = toDoContent, + subject = selectedSubjectId, + customSubject = subjectContent, + priority = priorityState, + isCompleted = isCompleted + ) + + /** + * 检查待办是否被编辑修改 + */ + fun isModified(): Boolean { + var isModified = false + if ((initialTodo?.content ?: "") != toDoContent) isModified = true + if ((initialTodo?.subject ?: 0) != selectedSubjectId) isModified = true + if ((initialTodo?.customSubject ?: "") != subjectContent) isModified = true + if ((initialTodo?.priority ?: 0f) != priorityState) isModified = true + if ((initialTodo?.isCompleted == true) != isCompleted) isModified = true + return isModified + } + + /** + * 保存状态的 Saver 对象,用于适配 rememberSaveable + */ + object Saver : androidx.compose.runtime.saveable.Saver { + override fun SaverScope.save(value: EditorState): Any? { + return listOf( + value.initialTodo?.id ?: 0, + value.toDoContent, + value.isErrorContent, + value.selectedSubjectId, + value.subjectContent, + value.isErrorSubject, + value.priorityState, + value.isCompleted, + value.showExitConfirmDialog, + value.showDeleteConfirmDialog + ) + } + + override fun restore(value: Any): EditorState? { + val list = value as List<*> + val initialTodo = list[0] as? TodoEntity? + return EditorState(initialTodo).apply { + toDoContent = list[1] as String + isErrorContent = list[2] as Boolean + selectedSubjectId = list[3] as Int + subjectContent = list[4] as String + isErrorSubject = list[5] as Boolean + priorityState = list[6] as Float + isCompleted = list[7] as Boolean + showExitConfirmDialog = list[8] as Boolean + showDeleteConfirmDialog = list[9] as Boolean + } + } + } +} + +@Composable +fun rememberEditorState(initialTodo: TodoEntity? = null): EditorState = + rememberSaveable(saver = EditorState.Saver) { EditorState(initialTodo) } \ No newline at end of file