feat: 支持设置任务优先级

This commit is contained in:
Super12138 2025-01-17 14:39:10 +08:00
parent e962a9f361
commit 08d7706b15
11 changed files with 213 additions and 53 deletions

View file

@ -9,5 +9,6 @@ data class TodoEntity(
@ColumnInfo(name = "content") val content: String, @ColumnInfo(name = "content") val content: String,
@ColumnInfo(name = "subject") val subject: Int, @ColumnInfo(name = "subject") val subject: Int,
@ColumnInfo(name = "completed") val isCompleted: Boolean = false, @ColumnInfo(name = "completed") val isCompleted: Boolean = false,
@ColumnInfo(name = "priority") val priority: Float,
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Int = 0, @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") val id: Int = 0,
) )

View file

@ -56,7 +56,7 @@ fun FilterChipGroup(
label = { label = {
Text(item) Text(item)
}, },
modifier = Modifier.padding(horizontal = 5.dp) modifier = Modifier.padding(end = 10.dp)
) )
} }
} }

View file

@ -8,9 +8,11 @@ import androidx.compose.foundation.layout.width
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.FloatingActionButtonDefaults import androidx.compose.material3.FloatingActionButtonDefaults
import androidx.compose.material3.FloatingActionButtonElevation import androidx.compose.material3.FloatingActionButtonElevation
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
/** /**
@ -28,14 +30,18 @@ import androidx.compose.ui.unit.dp
fun AnimatedExtendedFloatingActionButton( fun AnimatedExtendedFloatingActionButton(
icon: @Composable () -> Unit, icon: @Composable () -> Unit,
text: @Composable () -> Unit, text: @Composable () -> Unit,
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
expanded: Boolean, expanded: Boolean,
containerColor: Color = FloatingActionButtonDefaults.containerColor,
contentColor: Color = contentColorFor(containerColor),
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
FloatingActionButton( FloatingActionButton(
onClick = onClick, onClick = onClick,
elevation = elevation, elevation = elevation,
containerColor = containerColor,
contentColor = contentColor,
modifier = modifier modifier = modifier
) { ) {
Row( Row(

View file

@ -4,6 +4,8 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.exclude
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.safeContent import androidx.compose.foundation.layout.safeContent
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack import androidx.compose.material.icons.automirrored.outlined.ArrowBack
@ -41,7 +43,7 @@ fun LargeTopAppBarScaffold(
snackbarHost: @Composable () -> Unit = {}, snackbarHost: @Composable () -> Unit = {},
floatingActionButton: @Composable () -> Unit = {}, floatingActionButton: @Composable () -> Unit = {},
floatingActionButtonPosition: FabPosition = FabPosition.End, floatingActionButtonPosition: FabPosition = FabPosition.End,
contentWindowInsets: WindowInsets = WindowInsets.safeContent, contentWindowInsets: WindowInsets = WindowInsets.safeContent.exclude(WindowInsets.ime),
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
content: @Composable (PaddingValues) -> Unit content: @Composable (PaddingValues) -> Unit
) { ) {
@ -62,6 +64,8 @@ fun LargeTopAppBarScaffold(
} }
}, },
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
floatingActionButton = floatingActionButton,
floatingActionButtonPosition = floatingActionButtonPosition,
contentWindowInsets = contentWindowInsets contentWindowInsets = contentWindowInsets
) { innerPadding -> ) { innerPadding ->
Box { content(innerPadding) } Box { content(innerPadding) }

View file

@ -62,6 +62,7 @@ fun TodoNavigation(
toDo = viewModel.selectedEditTodo, toDo = viewModel.selectedEditTodo,
onSave = { onSave = {
viewModel.addTodo(it) viewModel.addTodo(it)
viewModel.setEditTodoItem(null)
navController.navigateUp() navController.navigateUp()
}, },
onDelete = { onDelete = {
@ -72,8 +73,8 @@ fun TodoNavigation(
navController.navigateUp() navController.navigateUp()
}, },
onNavigateUp = { onNavigateUp = {
viewModel.setEditTodoItem(null)
navController.navigateUp() navController.navigateUp()
viewModel.setEditTodoItem(null)
} }
) )
} }

View file

@ -1,6 +1,8 @@
package cn.super12138.todo.ui.pages.editor package cn.super12138.todo.ui.pages.editor
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -9,17 +11,26 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ButtonColors import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.Save
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon
import androidx.compose.material3.Label
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.PlainTooltip
import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextField import androidx.compose.material3.TextField
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -28,13 +39,17 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import cn.super12138.todo.R import cn.super12138.todo.R
import cn.super12138.todo.logic.database.TodoEntity import cn.super12138.todo.logic.database.TodoEntity
import cn.super12138.todo.logic.model.Subjects import cn.super12138.todo.logic.model.Subjects
import cn.super12138.todo.ui.TodoDefaults import cn.super12138.todo.ui.TodoDefaults
import cn.super12138.todo.ui.components.AnimatedExtendedFloatingActionButton
import cn.super12138.todo.ui.components.FilterChipGroup import cn.super12138.todo.ui.components.FilterChipGroup
import cn.super12138.todo.ui.components.LargeTopAppBarScaffold import cn.super12138.todo.ui.components.LargeTopAppBarScaffold
import cn.super12138.todo.ui.pages.main.components.getPriorityString
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@ -45,11 +60,65 @@ fun TodoEditorPage(
onNavigateUp: () -> Unit, onNavigateUp: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
BackHandler {
onNavigateUp()
}
val context = LocalContext.current val context = LocalContext.current
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
var text by rememberSaveable { mutableStateOf(toDo?.content ?: "") }
var isError by rememberSaveable { mutableStateOf(false) }
var selectedItemIndex by rememberSaveable { mutableIntStateOf(toDo?.subject ?: 0) }
var sliderPosition by rememberSaveable { mutableFloatStateOf(toDo?.priority ?: 0f) }
LargeTopAppBarScaffold( LargeTopAppBarScaffold(
title = stringResource(if (toDo != null) R.string.title_edit_task else R.string.action_add_task), title = stringResource(if (toDo != null) R.string.title_edit_task else R.string.action_add_task),
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
floatingActionButton = {
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) {
if (toDo !== null) {
AnimatedExtendedFloatingActionButton(
icon = {
Icon(
imageVector = Icons.Outlined.Delete,
contentDescription = stringResource(R.string.action_delete)
)
},
text = { Text(stringResource(R.string.action_delete)) },
expanded = true,
containerColor = MaterialTheme.colorScheme.errorContainer,
onClick = onDelete
)
}
AnimatedExtendedFloatingActionButton(
icon = {
Icon(
imageVector = Icons.Outlined.Save,
contentDescription = stringResource(R.string.action_save)
)
},
text = { Text(stringResource(R.string.action_save)) },
expanded = true,
onClick = {
if (text.trim().isEmpty()) {
isError = true
return@AnimatedExtendedFloatingActionButton
}
isError = false
onSave(
TodoEntity(
content = text,
subject = selectedItemIndex,
isCompleted = toDo?.isCompleted ?: false,
priority = sliderPosition,
id = toDo?.id ?: 0
)
)
}
)
}
},
onBack = onNavigateUp, onBack = onNavigateUp,
modifier = modifier modifier = modifier
) { innerPadding -> ) { innerPadding ->
@ -60,8 +129,6 @@ fun TodoEditorPage(
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())
) { ) {
var text by rememberSaveable { mutableStateOf(toDo?.content ?: "") }
var isError by rememberSaveable { mutableStateOf(false) }
TextField( TextField(
value = text, value = text,
onValueChange = { text = it }, onValueChange = { text = it },
@ -82,11 +149,13 @@ fun TodoEditorPage(
it.getDisplayName(context) it.getDisplayName(context)
} }
} }
var selectedItemIndex by rememberSaveable { mutableIntStateOf(toDo?.subject ?: 0) }
Text( Text(
text = stringResource(R.string.label_subject), text = stringResource(R.string.label_subject),
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium
) )
Spacer(Modifier.size(5.dp))
FilterChipGroup( FilterChipGroup(
items = subjects, items = subjects,
defaultSelectedItemIndex = toDo?.subject ?: 0, defaultSelectedItemIndex = toDo?.subject ?: 0,
@ -96,47 +165,45 @@ fun TodoEditorPage(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) )
Spacer(Modifier.size(20.dp)) Spacer(Modifier.size(10.dp))
// 操作按钮 Text(
Row(horizontalArrangement = Arrangement.spacedBy(10.dp)) { text = stringResource(R.string.label_priority),
if (toDo !== null) { style = MaterialTheme.typography.titleMedium
FilledTonalButton( )
onClick = {
onDelete() Spacer(Modifier.size(5.dp))
val interactionSource = remember { MutableInteractionSource() }
Slider(
modifier = Modifier.semantics {
contentDescription = context.getString(R.string.label_priority)
},
value = sliderPosition,
onValueChange = { sliderPosition = it },
valueRange = -10f..10f,
steps = 3,
interactionSource = interactionSource,
thumb = {
Label(
label = {
PlainTooltip(
modifier = Modifier
.sizeIn(45.dp, 25.dp)
.wrapContentWidth()
) {
Text(stringResource(getPriorityString(sliderPosition)))
}
}, },
colors = ButtonColors( interactionSource = interactionSource
containerColor = MaterialTheme.colorScheme.errorContainer,
contentColor = MaterialTheme.colorScheme.onErrorContainer,
disabledContainerColor = MaterialTheme.colorScheme.onSurface,
disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant
)
) { ) {
Text(stringResource(R.string.action_delete)) SliderDefaults.Thumb(interactionSource)
} }
} }
FilledTonalButton( )
onClick = {
// 文本为空
if (text.trim().isEmpty()) {
isError = true
return@FilledTonalButton
}
isError = false Spacer(Modifier.size(40.dp))
onSave(
TodoEntity(
content = text,
subject = selectedItemIndex,
isCompleted = toDo?.isCompleted ?: false,
id = toDo?.id ?: 0
)
)
}
) {
Text(stringResource(R.string.action_save))
}
}
} }
} }
} }

View file

@ -125,6 +125,7 @@ fun MainPage(
content = content, content = content,
subject = subject, subject = subject,
isCompleted = true, isCompleted = true,
priority = priority,
id = id id = id
) )
) )
@ -166,6 +167,7 @@ fun MainPage(
content = content, content = content,
subject = subject, subject = subject,
isCompleted = true, isCompleted = true,
priority = priority,
id = id id = id
) )
) )

View file

@ -69,10 +69,11 @@ fun ManagerFragment(
content = item.content, content = item.content,
subject = Subjects.fromId(item.subject).getDisplayName(context), subject = Subjects.fromId(item.subject).getDisplayName(context),
completed = item.isCompleted, completed = item.isCompleted,
priority = item.priority,
selected = selectedTodoIds.contains(item.id),
onCardClick = { onItemClick(item) }, onCardClick = { onItemClick(item) },
onCardLongClick = { onItemLongClick(item) }, onCardLongClick = { onItemLongClick(item) },
onChecked = { onItemChecked(item) }, onChecked = { onItemChecked(item) },
selected = selectedTodoIds.contains(item.id),
modifier = Modifier modifier = Modifier
.padding(vertical = 5.dp) .padding(vertical = 5.dp)
.animateItem() // TODO: 设置动画时间 .animateItem() // TODO: 设置动画时间

View file

@ -26,8 +26,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import cn.super12138.todo.R import cn.super12138.todo.R
@ -37,10 +39,11 @@ fun TodoCard(
content: String, content: String,
subject: String, subject: String,
completed: Boolean, completed: Boolean,
priority: Float,
selected: Boolean,
onCardClick: () -> Unit = {}, onCardClick: () -> Unit = {},
onCardLongClick: () -> Unit = {}, onCardLongClick: () -> Unit = {},
onChecked: () -> Unit = {}, onChecked: () -> Unit = {},
selected: Boolean,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
ElevatedCard( ElevatedCard(
@ -85,14 +88,27 @@ fun TodoCard(
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None, textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None,
modifier = Modifier.basicMarquee() // TODO: 后续评估性能影响 modifier = Modifier
) .fillMaxWidth()
Text( .basicMarquee() // TODO: 后续评估性能影响
text = subject,
style = MaterialTheme.typography.labelMedium,
textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None,
maxLines = 1
) )
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier.fillMaxWidth()
) {
Text(
text = subject,
style = MaterialTheme.typography.labelMedium,
textDecoration = if (completed) TextDecoration.LineThrough else TextDecoration.None,
maxLines = 1
)
PriorityText(
priority = priority,
isCompleted = completed
)
}
} }
AnimatedVisibility(!selected && !completed) { AnimatedVisibility(!selected && !completed) {
@ -123,4 +139,54 @@ fun TodoCard(
}*/ }*/
} }
} }
}
@Composable
fun PriorityText(
priority: Float,
isCompleted: Boolean,
modifier: Modifier = Modifier
) {
Text(
text = stringResource(getPriorityString(priority)),
style = MaterialTheme.typography.labelMedium.copy(
fontWeight = FontWeight.Bold,
color = when (priority) {
-10f -> MaterialTheme.colorScheme.outline
-5f -> MaterialTheme.colorScheme.onSurfaceVariant
0f -> MaterialTheme.colorScheme.secondary
5f -> MaterialTheme.colorScheme.tertiary
10f -> MaterialTheme.colorScheme.onErrorContainer
else -> MaterialTheme.colorScheme.secondary
}
),
textDecoration = if (isCompleted) TextDecoration.LineThrough else TextDecoration.None,
maxLines = 1
)
}
fun getPriorityString(priority: Float): Int {
return when (priority) {
-10f -> R.string.priority_not_urgent
-5f -> R.string.priority_not_important
0f -> R.string.priority_default
5f -> R.string.priority_important
10f -> R.string.priority_urgent
else -> R.string.priority_default
}
}
@Preview(locale = "zh-rCN", showBackground = true)
@Composable
private fun TodoCardPreview() {
TodoCard(
content = "背《岳阳楼记》《出师表》《琵琶行》",
subject = "语文",
completed = false,
priority = 5f,
selected = false,
onCardClick = {},
onCardLongClick = {},
onChecked = {}
)
} }

View file

@ -40,4 +40,10 @@
<string name="pref_licence">开放源代码许可</string> <string name="pref_licence">开放源代码许可</string>
<string name="pref_licence_desc">查看应用使用的开源库及其许可</string> <string name="pref_licence_desc">查看应用使用的开源库及其许可</string>
<string name="label_subject">学科</string> <string name="label_subject">学科</string>
<string name="priority_not_urgent">不紧急</string>
<string name="priority_not_important">不重要</string>
<string name="priority_default">默认</string>
<string name="priority_important">重要</string>
<string name="priority_urgent">紧急</string>
<string name="label_priority">优先级</string>
</resources> </resources>

View file

@ -41,4 +41,10 @@
<string name="pref_licence">Open Source Licences</string> <string name="pref_licence">Open Source Licences</string>
<string name="pref_licence_desc">Check the open source libraries used by the application and their licences.</string> <string name="pref_licence_desc">Check the open source libraries used by the application and their licences.</string>
<string name="label_subject">Subject</string> <string name="label_subject">Subject</string>
<string name="priority_not_urgent">Not Urgent</string>
<string name="priority_not_important">Not Important</string>
<string name="priority_default">Default</string>
<string name="priority_important">Important</string>
<string name="priority_urgent">Urgent</string>
<string name="label_priority">Priority</string>
</resources> </resources>