diff --git a/app/schemas/cn.super12138.todo.logic.database.TodoDatabase/5.json b/app/schemas/cn.super12138.todo.logic.database.TodoDatabase/5.json new file mode 100644 index 0000000..9416f96 --- /dev/null +++ b/app/schemas/cn.super12138.todo.logic.database.TodoDatabase/5.json @@ -0,0 +1,60 @@ +{ + "formatVersion": 1, + "database": { + "version": 5, + "identityHash": "2ca52b6f8ef7ce2db985716407a7945c", + "entities": [ + { + "tableName": "todo", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`content` TEXT NOT NULL, `category` TEXT NOT NULL, `completed` INTEGER NOT NULL, `priority` REAL NOT NULL, `due_date` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)", + "fields": [ + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isCompleted", + "columnName": "completed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "priority", + "columnName": "priority", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "dueDate", + "columnName": "due_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2ca52b6f8ef7ce2db985716407a7945c')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoDatabase.kt b/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoDatabase.kt index d6569d9..bd4c2f1 100644 --- a/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoDatabase.kt +++ b/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoDatabase.kt @@ -8,7 +8,7 @@ import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase import cn.super12138.todo.constants.Constants -@Database(entities = [TodoEntity::class], version = 4) +@Database(entities = [TodoEntity::class], version = 5) abstract class TodoDatabase : RoomDatabase() { abstract fun toDoDao(): TodoDao @@ -22,7 +22,7 @@ abstract class TodoDatabase : RoomDatabase() { TodoDatabase::class.java, Constants.DB_NAME ) - .addMigrations(MIGRATION_2_3, MIGRATION_3_4) + .addMigrations(MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5) .fallbackToDestructiveMigration(false) .build() @@ -50,5 +50,11 @@ abstract class TodoDatabase : RoomDatabase() { db.execSQL("ALTER TABLE todo_new RENAME TO todo") } } + + private val MIGRATION_4_5 = object : Migration(4, 5) { + override fun migrate(db: SupportSQLiteDatabase) { + db.execSQL("ALTER TABLE ${Constants.DB_TABLE_NAME} ADD COLUMN due_date INTEGER") + } + } } } \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoEntity.kt b/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoEntity.kt index 26f386a..aed0265 100644 --- a/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoEntity.kt +++ b/app/src/main/kotlin/cn/super12138/todo/logic/database/TodoEntity.kt @@ -13,5 +13,6 @@ data class TodoEntity( @ColumnInfo(name = "category") val category: String = "", @ColumnInfo(name = "completed") val isCompleted: Boolean = false, @ColumnInfo(name = "priority") val priority: Float, + @ColumnInfo(name = "due_date") val dueDate: Long? = null, @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Int = 0, ) diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TodoDestinations.kt b/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TodoDestinations.kt index 096f2eb..bb65f7b 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TodoDestinations.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TodoDestinations.kt @@ -13,8 +13,8 @@ enum class TodoDestinations( Overview( route = TodoScreen.Overview, label = cn.super12138.todo.R.string.page_overview, - icon = cn.super12138.todo.R.drawable.ic_view_kanban, - selectedIcon = cn.super12138.todo.R.drawable.ic_view_kanban_filled + icon = cn.super12138.todo.R.drawable.ic_dashboard, + selectedIcon = cn.super12138.todo.R.drawable.ic_dashboard_filled ), Tasks( route = TodoScreen.Tasks, diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TopNavigation.kt b/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TopNavigation.kt index 18b01f6..00d1c87 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TopNavigation.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TopNavigation.kt @@ -2,7 +2,6 @@ package cn.super12138.todo.ui.navigation import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.SharedTransitionLayout -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.navigation3.runtime.NavKey @@ -78,7 +77,7 @@ fun TopNavigation( }, entryProvider = entryProvider { entry { - OverviewPage() + OverviewPage(viewModel = viewModel) } entry { 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 7294b3a..2cd6a99 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 @@ -22,7 +22,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.key import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue @@ -42,6 +41,7 @@ import cn.super12138.todo.ui.components.TopAppBarScaffold 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.TodoDueDateChooser import cn.super12138.todo.ui.pages.editor.components.TodoMarkAsCompletedCheckbox import cn.super12138.todo.ui.pages.editor.components.TodoPrioritySlider import cn.super12138.todo.ui.pages.editor.state.rememberEditorState @@ -81,7 +81,7 @@ fun SharedTransitionScope.TodoEditPage( ) @Composable -private fun TodoEditorPage( +fun TodoEditorPage( modifier: Modifier = Modifier, toDo: TodoEntity? = null, onSave: (TodoEntity) -> Unit, @@ -133,53 +133,54 @@ private fun TodoEditorPage( TopAppBarScaffold( title = stringResource(if (toDo != null) R.string.title_edit_task else R.string.action_add_task), floatingActionButton = { - Row( - horizontalArrangement = Arrangement.spacedBy(10.dp), - modifier = Modifier.imePadding() - ) { - if (toDo !== null) { - TodoFloatingActionButton( - text = stringResource(R.string.action_delete), - iconRes = R.drawable.ic_delete, - expanded = true, - containerColor = MaterialTheme.colorScheme.errorContainer, - onClick = { uiState.showDeleteConfirmDialog = true } - ) - } + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp), + modifier = Modifier.imePadding() + ) { + if (toDo !== null) { TodoFloatingActionButton( - text = stringResource(R.string.action_save), - iconRes = R.drawable.ic_save, + text = stringResource(R.string.action_delete), + iconRes = R.drawable.ic_delete, expanded = true, - onClick = { - if (uiState.setErrorIfNotValid()) { - return@TodoFloatingActionButton - } else { - uiState.clearError() - val newTodo = TodoEntity( - id = toDo?.id ?: 0, - content = uiState.toDoContent, - category = if (isCustomCategory) uiState.categoryContent else categories[uiState.selectedCategoryIndex].name, - priority = uiState.priorityState, - isCompleted = uiState.isCompleted - ) - onSave(newTodo) - } - } + containerColor = MaterialTheme.colorScheme.errorContainer, + onClick = { uiState.showDeleteConfirmDialog = true } ) } + TodoFloatingActionButton( + text = stringResource(R.string.action_save), + iconRes = R.drawable.ic_save, + expanded = true, + onClick = { + if (uiState.setErrorIfNotValid()) { + return@TodoFloatingActionButton + } else { + uiState.clearError() + val newTodo = TodoEntity( + id = toDo?.id ?: 0, + content = uiState.toDoContent, + category = if (isCustomCategory) uiState.categoryContent else categories[uiState.selectedCategoryIndex].name, + priority = uiState.priorityState, + dueDate = uiState.dueDateState, + isCompleted = uiState.isCompleted + ) + onSave(newTodo) + } + } + ) + } }, onBack = ::checkModifiedBeforeBack, modifier = modifier ) { LazyColumn( - verticalArrangement = Arrangement.spacedBy(5.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier.fillMaxSize() ) { item(key = 0) { Spacer(modifier = Modifier.size(TodoDefaults.screenVerticalPadding)) } - item(key=1) { + item(key = 1) { TodoContentTextField( value = uiState.toDoContent, onValueChange = { uiState.toDoContent = it }, @@ -217,7 +218,7 @@ private fun TodoEditorPage( } } - item (key = 3){ + item(key = 3) { Text( text = stringResource(R.string.label_priority), style = MaterialTheme.typography.titleMedium @@ -230,12 +231,16 @@ private fun TodoEditorPage( } item(key = 4) { + Text( + text = stringResource(R.string.label_more), + style = MaterialTheme.typography.titleMedium + ) + TodoDueDateChooser( + value = uiState.dueDateState, + onValueChange = { uiState.dueDateState = it }, + modifier = Modifier.fillMaxWidth() + ) if (toDo != null) { - Text( - text = stringResource(R.string.label_more), - style = MaterialTheme.typography.titleMedium - ) - TodoMarkAsCompletedCheckbox( checked = uiState.isCompleted, onCheckedChange = { uiState.isCompleted = it }, @@ -244,28 +249,28 @@ private fun TodoEditorPage( } } - item (key = 5){ + item(key = 5) { Spacer(modifier = Modifier.size(TodoDefaults.screenVerticalPadding)) } } - - ConfirmDialog( - visible = uiState.showExitConfirmDialog, - iconRes = R.drawable.ic_undo, - text = stringResource(R.string.tip_discard_changes), - onConfirm = { - uiState.showExitConfirmDialog = false - onNavigateUp() - }, - onDismiss = { uiState.showExitConfirmDialog = false } - ) - - ConfirmDialog( - visible = uiState.showDeleteConfirmDialog, - iconRes = R.drawable.ic_delete, - text = stringResource(R.string.tip_delete_task, 1), - onConfirm = onDelete, - onDismiss = { uiState.showDeleteConfirmDialog = false } - ) } + + ConfirmDialog( + visible = uiState.showExitConfirmDialog, + iconRes = R.drawable.ic_undo, + text = stringResource(R.string.tip_discard_changes), + onConfirm = { + uiState.showExitConfirmDialog = false + onNavigateUp() + }, + onDismiss = { uiState.showExitConfirmDialog = false } + ) + + ConfirmDialog( + visible = uiState.showDeleteConfirmDialog, + iconRes = R.drawable.ic_delete, + text = stringResource(R.string.tip_delete_task, 1), + onConfirm = onDelete, + onDismiss = { uiState.showDeleteConfirmDialog = false } + ) } \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoDueDateChooser.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoDueDateChooser.kt new file mode 100644 index 0000000..2d3fe13 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoDueDateChooser.kt @@ -0,0 +1,148 @@ +package cn.super12138.todo.ui.pages.editor.components + +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.DatePicker +import androidx.compose.material3.DatePickerDialog +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TextField +import androidx.compose.material3.rememberDatePickerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.stringResource +import cn.super12138.todo.R +import cn.super12138.todo.utils.VibrationUtils +import cn.super12138.todo.utils.toLocalDateString + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun TodoDueDateChooser( + value: Long?, + onValueChange: (Long?) -> Unit, + modifier: Modifier = Modifier +) { + val view = LocalView.current + + val datePickerState = rememberDatePickerState(initialSelectedDateMillis = value) + val openDialog = remember { mutableStateOf(false) } + + val interactionSource = remember { MutableInteractionSource() } + val pressed by interactionSource.collectIsPressedAsState() + LaunchedEffect(pressed) { + if (pressed) { + VibrationUtils.performHapticFeedback(view) + openDialog.value = true + } + } + + TextField( + value = value.toLocalDateString(), + onValueChange = {}, + label = { Text("截止日期(可选)") }, + readOnly = true, + interactionSource = interactionSource, + modifier = modifier + ) + + + /*val interactionSource = remember { MutableInteractionSource() } + val pressed by interactionSource.collectIsPressedAsState() + val animatedShape = shapeByInteraction( + ButtonDefaults.shapes(), + pressed, + TodoDefaults.shapesDefaultAnimationSpec + )*/ + /*Surface( + modifier = modifier + .semantics { role = Role.Button } + .weight(1f), + color = ButtonDefaults.filledTonalButtonColors().containerColor, + shape = animatedShape + ) { + Row( + modifier = Modifier + .defaultMinSize( + minWidth = ButtonDefaults.MinWidth, + minHeight = ButtonDefaults.MinHeight, + ) + .combinedClickable( + interactionSource = interactionSource, + onClick = { + VibrationUtils.performHapticFeedback(view) + openDialog = true + }, + onLongClick = { onValueChange(null) } + ) + .padding(ButtonDefaults.ContentPadding), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = "选择", + style = MaterialTheme.typography.labelLarge + ) + } + }*/ + + + if (openDialog.value) { + DatePickerDialog( + confirmButton = { + FilledTonalButton( + onClick = { + VibrationUtils.performHapticFeedback(view) + onValueChange(datePickerState.selectedDateMillis) + openDialog.value = false + }, + shapes = ButtonDefaults.shapes(), + ) { + Text(stringResource(R.string.action_confirm)) + } + }, + dismissButton = { + Row { + TextButton( + onClick = { + VibrationUtils.performHapticFeedback(view) + onValueChange(null) + datePickerState.selectedDateMillis = null + }, + shapes = ButtonDefaults.shapes() + ) { + Text("清除") + } + TextButton( + onClick = { + VibrationUtils.performHapticFeedback(view) + openDialog.value = false + }, + shapes = ButtonDefaults.shapes() + ) { + Text(stringResource(R.string.action_cancel)) + } + } + }, + onDismissRequest = { + onValueChange(null) + openDialog.value = false + } + ) { + DatePicker( + state = datePickerState, + modifier = Modifier.verticalScroll(rememberScrollState()), + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoEditorTextFields.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoEditorTextFields.kt index 457a096..4adbb4b 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoEditorTextFields.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/editor/components/TodoEditorTextFields.kt @@ -42,7 +42,7 @@ fun TodoCategoryTextField( value: String, onValueChange: (String) -> Unit, isError: Boolean, - supportingText: String = stringResource(R.string.tip_max_length_5), + supportingText: String = stringResource(R.string.tip_short_category), modifier: Modifier = Modifier ) { TextField( 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 index 99d9824..3b9bfdd 100644 --- 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 @@ -18,9 +18,9 @@ class EditorState(val initialTodo: TodoEntity? = null) { var categoryContent by mutableStateOf(initialTodo?.category ?: "") var isErrorCategory by mutableStateOf(false) var priorityState by mutableFloatStateOf(initialTodo?.priority ?: 0f) + var dueDateState by mutableStateOf(initialTodo?.dueDate) var isCompleted by mutableStateOf(initialTodo?.isCompleted == true) - - var categorySupportingText by mutableIntStateOf(R.string.tip_max_length_5) + var categorySupportingText by mutableIntStateOf(R.string.tip_short_category) private set var showExitConfirmDialog by mutableStateOf(false) @@ -33,17 +33,9 @@ class EditorState(val initialTodo: TodoEntity? = null) { */ fun setErrorIfNotValid(): Boolean { isErrorContent = toDoContent.trim().isEmpty() - if (selectedCategoryIndex == -1) { - if (categoryContent.trim().isEmpty()) { + if (selectedCategoryIndex == -1 && categoryContent.trim().isEmpty()) { isErrorCategory = true categorySupportingText = R.string.error_no_content_entered - } else if (categoryContent.length > 5) { - isErrorCategory = true - categorySupportingText = R.string.error_exceeds_5_chars - } else { - isErrorCategory = false - categorySupportingText = R.string.tip_max_length_5 - } } else { isErrorCategory = false } @@ -67,6 +59,7 @@ class EditorState(val initialTodo: TodoEntity? = null) { if ((initialTodo?.category ?: "") != categoryContent) isModified = true if ((initialTodo?.priority ?: 0f) != priorityState) isModified = true if ((initialTodo?.isCompleted == true) != isCompleted) isModified = true + if (initialTodo?.dueDate != dueDateState) isModified = true return isModified } @@ -83,6 +76,7 @@ class EditorState(val initialTodo: TodoEntity? = null) { value.categoryContent, value.isErrorCategory, value.priorityState, + value.dueDateState, value.isCompleted, value.showExitConfirmDialog, value.showDeleteConfirmDialog @@ -99,9 +93,10 @@ class EditorState(val initialTodo: TodoEntity? = null) { categoryContent = list[4] as String isErrorCategory = list[5] as Boolean priorityState = list[6] as Float - isCompleted = list[7] as Boolean - showExitConfirmDialog = list[8] as Boolean - showDeleteConfirmDialog = list[9] as Boolean + dueDateState = list[7] as Long? + isCompleted = list[8] as Boolean + showExitConfirmDialog = list[9] as Boolean + showDeleteConfirmDialog = list[10] as Boolean } } } diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/overview/OverviewPage.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/overview/OverviewPage.kt index 65dfca8..6c39473 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/overview/OverviewPage.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/overview/OverviewPage.kt @@ -1,28 +1,76 @@ package cn.super12138.todo.ui.pages.overview -import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.material3.Card -import androidx.compose.material3.Icon -import androidx.compose.material3.Text +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid +import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import cn.super12138.todo.R import cn.super12138.todo.ui.components.TopAppBarScaffold +import cn.super12138.todo.ui.pages.overview.components.RoundedCornerCardLarge +import cn.super12138.todo.ui.pages.overview.components.UpcomingTaskCard +import cn.super12138.todo.ui.viewmodels.MainViewModel -@OptIn(ExperimentalAnimationApi::class) +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable -fun OverviewPage(modifier: Modifier = Modifier) { +fun OverviewPage( + viewModel: MainViewModel, + modifier: Modifier = Modifier +) { + val toDos by viewModel.sortedTodos.collectAsState(initial = emptyList()) + val totalTasks by remember { derivedStateOf { toDos.size } } + val completedTasks by remember { derivedStateOf { toDos.count { it.isCompleted } } } + val nextWeekTodo by remember { derivedStateOf { toDos.filter { it.dueDate != null && it.dueDate < System.currentTimeMillis() + 7 * 24 * 60 * 60 * 1000 } } } + TopAppBarScaffold( title = stringResource(R.string.page_overview), modifier = modifier ) { Column { - Card { - Icon(painter = painterResource(R.drawable.ic_ballot), contentDescription = null) - Text("总任务") + LazyVerticalStaggeredGrid( + modifier = Modifier.fillMaxSize(), + columns = StaggeredGridCells.Adaptive(150.dp), + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalItemSpacing = 10.dp + ) { + item { + RoundedCornerCardLarge( + iconRes = R.drawable.ic_apps, + title = "总任务", + count = totalTasks + ) + } + item { + RoundedCornerCardLarge( + iconRes = R.drawable.ic_check_circle, + title = "已完成", + count = completedTasks, + containerColor = MaterialTheme.colorScheme.secondaryContainer + ) + } + item { + RoundedCornerCardLarge( + iconRes = R.drawable.ic_pending, + title = "未完成", + count = totalTasks - completedTasks, + containerColor = MaterialTheme.colorScheme.errorContainer // tertiaryContainer + ) + } + item { + UpcomingTaskCard( + nextWeekTodo = nextWeekTodo + ) + } } } } diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/overview/components/StatusCard.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/overview/components/StatusCard.kt new file mode 100644 index 0000000..a6ec03e --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/overview/components/StatusCard.kt @@ -0,0 +1,82 @@ +package cn.super12138.todo.ui.pages.overview.components + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +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.material3.ButtonShapes +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import cn.super12138.todo.ui.TodoDefaults +import cn.super12138.todo.ui.theme.shapeByInteraction + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun RoundedCornerCardLarge( + modifier: Modifier = Modifier, + @DrawableRes iconRes: Int, + title: String, + count: Int, + containerColor: Color = TodoDefaults.ContainerColor, + shapes: ButtonShapes = TodoDefaults.shapes(), + onClick: () -> Unit = {} +) { + val interactionSource = remember { MutableInteractionSource() } + val pressed by interactionSource.collectIsPressedAsState() + val animatedShape = shapeByInteraction(shapes, pressed, TodoDefaults.shapesDefaultAnimationSpec) + + Card( + modifier = modifier.wrapContentHeight(), + colors = CardDefaults.cardColors(containerColor = containerColor), + shape = animatedShape + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable( + enabled = true, + onClick = onClick, + interactionSource = interactionSource + ) + .padding(TodoDefaults.screenHorizontalPadding), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + Icon( + painter = painterResource(iconRes), + contentDescription = null + ) + Column(horizontalAlignment = Alignment.Start) { + Text( + text = title, + style = MaterialTheme.typography.bodyMedium + ) + Text( + text = count.toString(), + style = MaterialTheme.typography.headlineLarge.copy( + fontWeight = FontWeight.ExtraBold + ) + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/overview/components/UpcomingTaskCard.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/overview/components/UpcomingTaskCard.kt new file mode 100644 index 0000000..3b8e41f --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/overview/components/UpcomingTaskCard.kt @@ -0,0 +1,63 @@ +package cn.super12138.todo.ui.pages.overview.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +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.graphics.Color +import androidx.compose.ui.unit.dp +import cn.super12138.todo.logic.database.TodoEntity +import cn.super12138.todo.ui.TodoDefaults +import cn.super12138.todo.ui.pages.settings.components.SettingsItem +import cn.super12138.todo.utils.toLocalDateString + +@Composable +fun UpcomingTaskCard( + nextWeekTodo: List, + containerColor: Color = TodoDefaults.ContainerColor, + modifier: Modifier = Modifier +) { + Card( + modifier = modifier.height(300.dp), + colors = CardDefaults.cardColors(containerColor = containerColor), + shape = TodoDefaults.defaultShape + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(TodoDefaults.screenHorizontalPadding), + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = "临近的任务", + style = MaterialTheme.typography.titleMedium + ) + + LazyColumn( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items( + items = nextWeekTodo, + key = { it.id } + ) { + SettingsItem( + title = it.content, + description = it.dueDate.toLocalDateString(), + enableClick = false + ) + } + } + } + } +} \ No newline at end of file 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 index aa59817..8690571 100644 --- 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 @@ -43,9 +43,8 @@ fun CategoryPromptDialog( } val supportingText = listOf( - stringResource(R.string.tip_max_length_5), + stringResource(R.string.tip_short_category), stringResource(R.string.error_no_content_entered), - stringResource(R.string.error_exceeds_5_chars) ) var currentSupportingText by remember { mutableStateOf(supportingText[0]) } @@ -70,26 +69,16 @@ fun CategoryPromptDialog( dismissButton = stringResource(R.string.action_cancel), 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(initialCategory, trimmedText.toString()) - isError = false - currentSupportingText = supportingText[0] - textFieldState.clearText() - onDismiss() - } + if (trimmedText.isEmpty()) { + isError = true + currentSupportingText = supportingText[1] + return@BasicDialog + } else { + onSave(initialCategory, trimmedText.toString()) + isError = false + currentSupportingText = supportingText[0] + textFieldState.clearText() + onDismiss() } }, onDismiss = onDismiss, diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/TasksPage.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/TasksPage.kt index 7443bff..a287c0f 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/TasksPage.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/TasksPage.kt @@ -51,7 +51,6 @@ fun SharedTransitionScope.TasksPage( ) { val animatedVisibilityScope = LocalNavAnimatedContentScope.current - val toDos = viewModel.sortedTodos.collectAsState(initial = emptyList()) val selectedTodos = viewModel.selectedTodoIds.collectAsState() val showCompleted by DataStoreManager.showCompletedFlow.collectAsState(initial = Constants.PREF_SHOW_COMPLETED_DEFAULT) @@ -60,9 +59,7 @@ fun SharedTransitionScope.TasksPage( val selectedTodoIds by remember { derivedStateOf { selectedTodos.value } } val inSelectedMode by remember { derivedStateOf { !selectedTodoIds.isEmpty() } } - val toDoList by remember { derivedStateOf { toDos.value } } - /*val totalTasks by remember { derivedStateOf { toDoList.size } } - val completedTasks by remember { derivedStateOf { toDoList.count { it.isCompleted } } }*/ + val toDoList by viewModel.sortedTodos.collectAsState(initial = emptyList()) val filteredTodoList = if (showCompleted) toDoList else toDoList.filter { item -> !item.isCompleted } val expandedFab by remember { derivedStateOf { listState.firstVisibleItemIndex == 0 } } diff --git a/app/src/main/kotlin/cn/super12138/todo/utils/TodoExt.kt b/app/src/main/kotlin/cn/super12138/todo/utils/TodoExt.kt index 8297445..c767bca 100644 --- a/app/src/main/kotlin/cn/super12138/todo/utils/TodoExt.kt +++ b/app/src/main/kotlin/cn/super12138/todo/utils/TodoExt.kt @@ -12,6 +12,8 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.ContentDrawScope import androidx.compose.ui.unit.Dp import cn.super12138.todo.logic.model.Priority +import java.text.SimpleDateFormat +import java.util.Date @Composable @Stable @@ -68,4 +70,17 @@ fun ContentDrawScope.drawFadedEdge( ), blendMode = BlendMode.DstIn ) +} + +/** + * 将时间戳转换为本地日期字符串 + * + * @receiver Long? 时间戳(单位为毫秒)或 null + * @return String 格式化后的日期字符串。如果为传入参数为null则返回空字符串,反之格式为 “yyyy-MM-dd” + */ +fun Long?.toLocalDateString(): String { + if (this == null) return "" + val date = Date(this) + val format = SimpleDateFormat("yyyy-MM-dd", java.util.Locale.getDefault()) + return format.format(date) } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_apps.xml b/app/src/main/res/drawable/ic_apps.xml new file mode 100644 index 0000000..5ce94da --- /dev/null +++ b/app/src/main/res/drawable/ic_apps.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_check_circle.xml b/app/src/main/res/drawable/ic_check_circle.xml new file mode 100644 index 0000000..c069f11 --- /dev/null +++ b/app/src/main/res/drawable/ic_check_circle.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_dashboard.xml b/app/src/main/res/drawable/ic_dashboard.xml new file mode 100644 index 0000000..d0f22d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_dashboard.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_dashboard_filled.xml b/app/src/main/res/drawable/ic_dashboard_filled.xml new file mode 100644 index 0000000..009c2a1 --- /dev/null +++ b/app/src/main/res/drawable/ic_dashboard_filled.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_pending.xml b/app/src/main/res/drawable/ic_pending.xml new file mode 100644 index 0000000..8cb30cf --- /dev/null +++ b/app/src/main/res/drawable/ic_pending.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_view_kanban.xml b/app/src/main/res/drawable/ic_view_kanban.xml deleted file mode 100644 index 68826de..0000000 --- a/app/src/main/res/drawable/ic_view_kanban.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_view_kanban_filled.xml b/app/src/main/res/drawable/ic_view_kanban_filled.xml deleted file mode 100644 index 79137ec..0000000 --- a/app/src/main/res/drawable/ic_view_kanban_filled.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 2a982eb..dd372be 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -105,8 +105,6 @@ إدارة فئات المهام إضافة فئة أدخل شيئًا - حتى 5 أحرف - تجاوز 5 أحرف أدخل الفئة التي تريد إضافتها لا توجد فئات مخصصة حاليًا. يمكنك إضافتها من الإعدادات. diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 99dc1c9..8c44870 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -104,8 +104,6 @@ 管理任务的分类标签 添加分类 输入内容 - 不超过 5 个字 - 超过 5 个字 输入你想添加的分类名称 当前暂无自定义分类,你可以在设置中添加分类 @@ -118,4 +116,5 @@ 已选择 待办 概览 + 尽量使用简短的分类名称 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5aa16fe..4488934 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -105,8 +105,6 @@ Manage the category of tasks Add Category Enter something - Up to 5 characters - Exceeds 5 characters Enter the category you want to add There are currently no custom categories. You can add categories in the settings. @@ -119,4 +117,5 @@ Selected Tasks Overview + Keep category names concise \ No newline at end of file