feat: 增加今日任务进度板块
This commit is contained in:
parent
1facdc28b7
commit
2959966552
7 changed files with 182 additions and 21 deletions
|
|
@ -7,10 +7,12 @@ import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
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.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
@ -26,8 +28,10 @@ enum class EmptyTipType {
|
||||||
@Composable
|
@Composable
|
||||||
fun EmptyTip(
|
fun EmptyTip(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
type: EmptyTipType,
|
||||||
size: Dp = 48.dp,
|
size: Dp = 48.dp,
|
||||||
type: EmptyTipType
|
containerColor: Color = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
|
contentColor: Color = contentColorFor(containerColor)
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
|
|
@ -35,7 +39,7 @@ fun EmptyTip(
|
||||||
.padding(TodoDefaults.screenHorizontalPadding)
|
.padding(TodoDefaults.screenHorizontalPadding)
|
||||||
.size(size)
|
.size(size)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(MaterialTheme.colorScheme.secondaryContainer)
|
.background(containerColor)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(
|
painter = painterResource(
|
||||||
|
|
@ -46,7 +50,7 @@ fun EmptyTip(
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = MaterialTheme.colorScheme.secondary,
|
tint = contentColor,
|
||||||
modifier = Modifier.size(size / 2)
|
modifier = Modifier.size(size / 2)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,9 @@ import androidx.compose.ui.res.stringResource
|
||||||
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.ui.components.TopAppBarScaffold
|
import cn.super12138.todo.ui.components.TopAppBarScaffold
|
||||||
|
import cn.super12138.todo.ui.pages.overview.components.ListCard
|
||||||
|
import cn.super12138.todo.ui.pages.overview.components.ProgressCard
|
||||||
import cn.super12138.todo.ui.pages.overview.components.RoundedCornerCardLarge
|
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
|
import cn.super12138.todo.ui.viewmodels.MainViewModel
|
||||||
import cn.super12138.todo.utils.SystemUtils
|
import cn.super12138.todo.utils.SystemUtils
|
||||||
|
|
||||||
|
|
@ -31,13 +32,26 @@ fun OverviewPage(
|
||||||
val toDos by viewModel.sortedTodos.collectAsState(initial = emptyList())
|
val toDos by viewModel.sortedTodos.collectAsState(initial = emptyList())
|
||||||
val totalTasks by remember { derivedStateOf { toDos.size } }
|
val totalTasks by remember { derivedStateOf { toDos.size } }
|
||||||
val completedTasks by remember { derivedStateOf { toDos.count { it.isCompleted } } }
|
val completedTasks by remember { derivedStateOf { toDos.count { it.isCompleted } } }
|
||||||
val nextWeekTodo by remember {
|
|
||||||
|
val todayMillis = SystemUtils.getToday()
|
||||||
|
val dayMillis = 24L * 60 * 60 * 1000
|
||||||
|
val todayEnd = todayMillis + dayMillis
|
||||||
|
|
||||||
|
val todayTodo by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
val today = SystemUtils.getToday()
|
|
||||||
val weekFromToday = today + 7L * 24 * 60 * 60 * 1000
|
|
||||||
toDos.filter { todo ->
|
toDos.filter { todo ->
|
||||||
val due = todo.dueDate ?: return@filter false
|
val due = todo.dueDate ?: return@filter false
|
||||||
due in today..<weekFromToday
|
due in todayMillis until todayEnd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val nextWeekTodo by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
val weekFromToday = todayMillis + 7 * dayMillis
|
||||||
|
toDos.filter { todo ->
|
||||||
|
val due = todo.dueDate ?: return@filter false
|
||||||
|
due in todayMillis until weekFromToday
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -74,12 +88,21 @@ fun OverviewPage(
|
||||||
iconRes = R.drawable.ic_pending,
|
iconRes = R.drawable.ic_pending,
|
||||||
title = stringResource(R.string.title_pending_task),
|
title = stringResource(R.string.title_pending_task),
|
||||||
count = totalTasks - completedTasks,
|
count = totalTasks - completedTasks,
|
||||||
containerColor = MaterialTheme.colorScheme.errorContainer // tertiaryContainer
|
containerColor = MaterialTheme.colorScheme.errorContainer
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
UpcomingTaskCard(
|
ProgressCard(
|
||||||
nextWeekTodo = nextWeekTodo
|
title = stringResource(R.string.title_today_task),
|
||||||
|
total = todayTodo.size,
|
||||||
|
completed = todayTodo.count { it.isCompleted }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
ListCard(
|
||||||
|
title = stringResource(R.string.title_upcoming_task),
|
||||||
|
list = nextWeekTodo
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,10 +40,12 @@ import cn.super12138.todo.utils.containerColor
|
||||||
import cn.super12138.todo.utils.toRelativeTimeString
|
import cn.super12138.todo.utils.toRelativeTimeString
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun UpcomingTaskCard(
|
fun ListCard(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
nextWeekTodo: List<TodoEntity>,
|
title: String,
|
||||||
containerColor: Color = TodoDefaults.Colors.Container
|
list: List<TodoEntity>,
|
||||||
|
containerColor: Color = TodoDefaults.Colors.Container,
|
||||||
|
emptyTipContainerColor: Color = MaterialTheme.colorScheme.secondaryContainer
|
||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
modifier = modifier.height(TodoDefaults.overviewCardHeight * 2),
|
modifier = modifier.height(TodoDefaults.overviewCardHeight * 2),
|
||||||
|
|
@ -58,12 +60,12 @@ fun UpcomingTaskCard(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.title_upcoming_task),
|
text = title,
|
||||||
style = MaterialTheme.typography.titleLarge
|
style = MaterialTheme.typography.titleLarge
|
||||||
)
|
)
|
||||||
val transitionSpec = fadeScale()
|
val transitionSpec = fadeScale()
|
||||||
AnimatedContent(
|
AnimatedContent(
|
||||||
targetState = nextWeekTodo.isEmpty(),
|
targetState = list.isEmpty(),
|
||||||
transitionSpec = { transitionSpec }
|
transitionSpec = { transitionSpec }
|
||||||
) {
|
) {
|
||||||
if (it) {
|
if (it) {
|
||||||
|
|
@ -75,7 +77,10 @@ fun UpcomingTaskCard(
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
EmptyTip(type = EmptyTipType.List)
|
EmptyTip(
|
||||||
|
type = EmptyTipType.List,
|
||||||
|
containerColor = emptyTipContainerColor
|
||||||
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.tip_no_task_brief),
|
text = stringResource(R.string.tip_no_task_brief),
|
||||||
|
|
@ -87,7 +92,7 @@ fun UpcomingTaskCard(
|
||||||
} else {
|
} else {
|
||||||
LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
items(
|
items(
|
||||||
items = nextWeekTodo,
|
items = list,
|
||||||
key = { task -> task.id }
|
key = { task -> task.id }
|
||||||
) { task ->
|
) { task ->
|
||||||
UpcomingTaskItem(
|
UpcomingTaskItem(
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
package cn.super12138.todo.ui.pages.overview.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
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.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.CircularWavyProgressIndicator
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ProgressIndicatorDefaults
|
||||||
|
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.graphics.StrokeCap
|
||||||
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import cn.super12138.todo.R
|
||||||
|
import cn.super12138.todo.ui.TodoDefaults
|
||||||
|
import cn.super12138.todo.ui.components.EmptyTip
|
||||||
|
import cn.super12138.todo.ui.components.EmptyTipType
|
||||||
|
import cn.super12138.todo.ui.theme.fadeScale
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ProgressCard(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
title: String,
|
||||||
|
total: Int,
|
||||||
|
completed: Int,
|
||||||
|
containerColor: Color = TodoDefaults.Colors.Container,
|
||||||
|
emptyTipContainerColor: Color = MaterialTheme.colorScheme.secondaryContainer
|
||||||
|
) {
|
||||||
|
val progress = if (total == 0) 0f else completed / total.toFloat()
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = modifier.height(TodoDefaults.overviewCardHeight * 2),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = containerColor),
|
||||||
|
shape = TodoDefaults.defaultShape
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(TodoDefaults.screenHorizontalPadding),
|
||||||
|
horizontalAlignment = Alignment.Start,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
|
val transitionSpec = fadeScale()
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = total == 0,
|
||||||
|
transitionSpec = { transitionSpec }
|
||||||
|
) {
|
||||||
|
if (it) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.weight(1f) // 占满剩余空间
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
EmptyTip(
|
||||||
|
type = EmptyTipType.List,
|
||||||
|
containerColor = emptyTipContainerColor
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.tip_no_task_brief),
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
val animatedProgress by animateFloatAsState(
|
||||||
|
targetValue = progress,
|
||||||
|
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
|
||||||
|
)
|
||||||
|
val thickStrokeWidth = with(LocalDensity.current) { 5.dp.toPx() }
|
||||||
|
val thickStroke = remember(thickStrokeWidth) {
|
||||||
|
Stroke(width = thickStrokeWidth, cap = StrokeCap.Round)
|
||||||
|
}
|
||||||
|
|
||||||
|
CircularWavyProgressIndicator(
|
||||||
|
progress = { animatedProgress },
|
||||||
|
waveSpeed = 3.dp,
|
||||||
|
wavelength = 20.dp,
|
||||||
|
stroke = thickStroke,
|
||||||
|
trackStroke = thickStroke,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(TodoDefaults.screenHorizontalPadding)
|
||||||
|
.size(90.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = "$completed / $total",
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -53,7 +53,6 @@ object SystemUtils {
|
||||||
set(Calendar.SECOND, 0)
|
set(Calendar.SECOND, 0)
|
||||||
set(Calendar.MILLISECOND, 0)
|
set(Calendar.MILLISECOND, 0)
|
||||||
}.timeInMillis
|
}.timeInMillis
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun ComponentActivity.configureEdgeToEdge() {
|
fun ComponentActivity.configureEdgeToEdge() {
|
||||||
|
|
|
||||||
|
|
@ -134,5 +134,5 @@
|
||||||
<string name="sorting_due_date">截止日期</string>
|
<string name="sorting_due_date">截止日期</string>
|
||||||
<string name="tip_search_task_not_found">暂未搜索到内容</string>
|
<string name="tip_search_task_not_found">暂未搜索到内容</string>
|
||||||
<string name="tip_no_task_brief">暂无任务</string>
|
<string name="tip_no_task_brief">暂无任务</string>
|
||||||
<string name="title_today_task">今日任务</string>
|
<string name="title_today_task">今日任务进度</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -135,5 +135,5 @@
|
||||||
<string name="sorting_due_date">Due date</string>
|
<string name="sorting_due_date">Due date</string>
|
||||||
<string name="tip_search_task_not_found">No tasks found</string>
|
<string name="tip_search_task_not_found">No tasks found</string>
|
||||||
<string name="tip_no_task_brief">No tasks</string>
|
<string name="tip_no_task_brief">No tasks</string>
|
||||||
<string name="title_today_task">Today</string>
|
<string name="title_today_task">Today\'s Task Progress</string>
|
||||||
</resources>
|
</resources>
|
||||||
Loading…
Reference in a new issue