feat: 初步增加截止日期功能 & 增加概览页面卡片 & 移除分类5字限制 & 优化导航栏图标

This commit is contained in:
Super12138 2026-02-07 08:45:32 +08:00
parent 1fabcaab9c
commit ddb02eb9a3
25 changed files with 572 additions and 141 deletions

View file

@ -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')"
]
}
}

View file

@ -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")
}
}
}
}

View file

@ -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,
)

View file

@ -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,

View file

@ -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<TodoScreen.Overview> {
OverviewPage()
OverviewPage(viewModel = viewModel)
}
entry<TodoScreen.Tasks> {

View file

@ -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 }
)
}

View file

@ -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()),
)
}
}
}

View file

@ -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(

View file

@ -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
}
}
}

View file

@ -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
)
}
}
}
}

View file

@ -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
)
)
}
}
}
}

View file

@ -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<TodoEntity>,
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
)
}
}
}
}
}

View file

@ -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,

View file

@ -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 } }

View file

@ -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)
}

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M183.5,776.5Q160,753 160,720Q160,687 183.5,663.5Q207,640 240,640Q273,640 296.5,663.5Q320,687 320,720Q320,753 296.5,776.5Q273,800 240,800Q207,800 183.5,776.5ZM423.5,776.5Q400,753 400,720Q400,687 423.5,663.5Q447,640 480,640Q513,640 536.5,663.5Q560,687 560,720Q560,753 536.5,776.5Q513,800 480,800Q447,800 423.5,776.5ZM663.5,776.5Q640,753 640,720Q640,687 663.5,663.5Q687,640 720,640Q753,640 776.5,663.5Q800,687 800,720Q800,753 776.5,776.5Q753,800 720,800Q687,800 663.5,776.5ZM183.5,536.5Q160,513 160,480Q160,447 183.5,423.5Q207,400 240,400Q273,400 296.5,423.5Q320,447 320,480Q320,513 296.5,536.5Q273,560 240,560Q207,560 183.5,536.5ZM423.5,536.5Q400,513 400,480Q400,447 423.5,423.5Q447,400 480,400Q513,400 536.5,423.5Q560,447 560,480Q560,513 536.5,536.5Q513,560 480,560Q447,560 423.5,536.5ZM663.5,536.5Q640,513 640,480Q640,447 663.5,423.5Q687,400 720,400Q753,400 776.5,423.5Q800,447 800,480Q800,513 776.5,536.5Q753,560 720,560Q687,560 663.5,536.5ZM183.5,296.5Q160,273 160,240Q160,207 183.5,183.5Q207,160 240,160Q273,160 296.5,183.5Q320,207 320,240Q320,273 296.5,296.5Q273,320 240,320Q207,320 183.5,296.5ZM423.5,296.5Q400,273 400,240Q400,207 423.5,183.5Q447,160 480,160Q513,160 536.5,183.5Q560,207 560,240Q560,273 536.5,296.5Q513,320 480,320Q447,320 423.5,296.5ZM663.5,296.5Q640,273 640,240Q640,207 663.5,183.5Q687,160 720,160Q753,160 776.5,183.5Q800,207 800,240Q800,273 776.5,296.5Q753,320 720,320Q687,320 663.5,296.5Z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M424,552L338,466Q327,455 310,455Q293,455 282,466Q271,477 271,494Q271,511 282,522L396,636Q408,648 424,648Q440,648 452,636L678,410Q689,399 689,382Q689,365 678,354Q667,343 650,343Q633,343 622,354L424,552ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M520,320L520,160Q520,143 531.5,131.5Q543,120 560,120L800,120Q817,120 828.5,131.5Q840,143 840,160L840,320Q840,337 828.5,348.5Q817,360 800,360L560,360Q543,360 531.5,348.5Q520,337 520,320ZM120,480L120,160Q120,143 131.5,131.5Q143,120 160,120L400,120Q417,120 428.5,131.5Q440,143 440,160L440,480Q440,497 428.5,508.5Q417,520 400,520L160,520Q143,520 131.5,508.5Q120,497 120,480ZM520,800L520,480Q520,463 531.5,451.5Q543,440 560,440L800,440Q817,440 828.5,451.5Q840,463 840,480L840,800Q840,817 828.5,828.5Q817,840 800,840L560,840Q543,840 531.5,828.5Q520,817 520,800ZM120,800L120,640Q120,623 131.5,611.5Q143,600 160,600L400,600Q417,600 428.5,611.5Q440,623 440,640L440,800Q440,817 428.5,828.5Q417,840 400,840L160,840Q143,840 131.5,828.5Q120,817 120,800ZM200,440L360,440L360,200L200,200L200,440ZM600,760L760,760L760,520L600,520L600,760ZM600,280L760,280L760,200L600,200L600,280ZM200,760L360,760L360,680L200,680L200,760ZM360,440L360,440L360,440L360,440ZM600,280L600,280L600,280L600,280ZM600,520L600,520L600,520L600,520ZM360,680L360,680L360,680L360,680Z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M560,360Q543,360 531.5,348.5Q520,337 520,320L520,160Q520,143 531.5,131.5Q543,120 560,120L800,120Q817,120 828.5,131.5Q840,143 840,160L840,320Q840,337 828.5,348.5Q817,360 800,360L560,360ZM160,520Q143,520 131.5,508.5Q120,497 120,480L120,160Q120,143 131.5,131.5Q143,120 160,120L400,120Q417,120 428.5,131.5Q440,143 440,160L440,480Q440,497 428.5,508.5Q417,520 400,520L160,520ZM560,840Q543,840 531.5,828.5Q520,817 520,800L520,480Q520,463 531.5,451.5Q543,440 560,440L800,440Q817,440 828.5,451.5Q840,463 840,480L840,800Q840,817 828.5,828.5Q817,840 800,840L560,840ZM160,840Q143,840 131.5,828.5Q120,817 120,800L120,640Q120,623 131.5,611.5Q143,600 160,600L400,600Q417,600 428.5,611.5Q440,623 440,640L440,800Q440,817 428.5,828.5Q417,840 400,840L160,840Z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M322.5,522.5Q340,505 340,480Q340,455 322.5,437.5Q305,420 280,420Q255,420 237.5,437.5Q220,455 220,480Q220,505 237.5,522.5Q255,540 280,540Q305,540 322.5,522.5ZM522.5,522.5Q540,505 540,480Q540,455 522.5,437.5Q505,420 480,420Q455,420 437.5,437.5Q420,455 420,480Q420,505 437.5,522.5Q455,540 480,540Q505,540 522.5,522.5ZM722.5,522.5Q740,505 740,480Q740,455 722.5,437.5Q705,420 680,420Q655,420 637.5,437.5Q620,455 620,480Q620,505 637.5,522.5Q655,540 680,540Q705,540 722.5,522.5ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q614,800 707,707Q800,614 800,480Q800,346 707,253Q614,160 480,160Q346,160 253,253Q160,346 160,480Q160,614 253,707Q346,800 480,800ZM480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Z"/>
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M280,680L360,680L360,280L280,280L280,680ZM600,600L680,600L680,280L600,280L600,600ZM440,480L520,480L520,280L440,280L440,480ZM200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM200,760L760,760Q760,760 760,760Q760,760 760,760L760,200Q760,200 760,200Q760,200 760,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760ZM200,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760L200,760Q200,760 200,760Q200,760 200,760L200,200Q200,200 200,200Q200,200 200,200Z" />
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM280,680L360,680L360,280L280,280L280,680ZM600,600L680,600L680,280L600,280L600,600ZM440,480L520,480L520,280L440,280L440,480Z" />
</vector>

View file

@ -105,8 +105,6 @@
<string name="pref_category_management_desc">إدارة فئات المهام</string>
<string name="action_add_category">إضافة فئة</string>
<string name="label_enter_sth">أدخل شيئًا</string>
<string name="tip_max_length_5">حتى 5 أحرف</string>
<string name="error_exceeds_5_chars">تجاوز 5 أحرف</string>
<string name="tip_enter_category">أدخل الفئة التي تريد إضافتها</string>
<!--<string name="error_category_duplicate">الفئة مكررة</string>-->
<string name="tip_no_category_chip">لا توجد فئات مخصصة حاليًا. يمكنك إضافتها من الإعدادات.</string>

View file

@ -104,8 +104,6 @@
<string name="pref_category_management_desc">管理任务的分类标签</string>
<string name="action_add_category">添加分类</string>
<string name="label_enter_sth">输入内容</string>
<string name="tip_max_length_5">不超过 5 个字</string>
<string name="error_exceeds_5_chars">超过 5 个字</string>
<string name="tip_enter_category">输入你想添加的分类名称</string>
<!--<string name="error_category_duplicate">该分类已经存在</string>-->
<string name="tip_no_category_chip">当前暂无自定义分类,你可以在设置中添加分类</string>
@ -118,4 +116,5 @@
<string name="tip_selected">已选择</string>
<string name="page_tasks">待办</string>
<string name="page_overview">概览</string>
<string name="tip_short_category">尽量使用简短的分类名称</string>
</resources>

View file

@ -105,8 +105,6 @@
<string name="pref_category_management_desc">Manage the category of tasks</string>
<string name="action_add_category">Add Category</string>
<string name="label_enter_sth">Enter something</string>
<string name="tip_max_length_5">Up to 5 characters</string>
<string name="error_exceeds_5_chars">Exceeds 5 characters</string>
<string name="tip_enter_category">Enter the category you want to add</string>
<!--<string name="error_category_duplicate">The category is duplicate</string>-->
<string name="tip_no_category_chip">There are currently no custom categories. You can add categories in the settings.</string>
@ -119,4 +117,5 @@
<string name="tip_selected">Selected</string>
<string name="page_tasks">Tasks</string>
<string name="page_overview">Overview</string>
<string name="tip_short_category">Keep category names concise</string>
</resources>