更新
This commit is contained in:
@@ -0,0 +1,258 @@
|
||||
package com.taskttl.presentation.countdown
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.CalendarToday
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.taskttl.core.ui.Chip
|
||||
import com.taskttl.core.utils.DateUtils
|
||||
import com.taskttl.data.state.CountdownEffect
|
||||
import com.taskttl.data.viewmodel.CountdownViewModel
|
||||
import com.taskttl.ui.components.AppHeader
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import taskttl.composeapp.generated.resources.Res
|
||||
import taskttl.composeapp.generated.resources.countdown_not_found
|
||||
import taskttl.composeapp.generated.resources.created_at
|
||||
import taskttl.composeapp.generated.resources.detail_information
|
||||
import taskttl.composeapp.generated.resources.event_description
|
||||
import taskttl.composeapp.generated.resources.label_days
|
||||
import taskttl.composeapp.generated.resources.reminder
|
||||
import taskttl.composeapp.generated.resources.title_countdown_info
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun CountdownDetailScreen(
|
||||
countdownId: String,
|
||||
onNavigateBack: () -> Unit,
|
||||
onNavigateToEdit: () -> Unit,
|
||||
viewModel: CountdownViewModel = koinViewModel()
|
||||
) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
val countdown = state.countdowns.find { it.id == countdownId }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.effects.collect { effect ->
|
||||
when (effect) {
|
||||
is CountdownEffect.NavigateBack -> {
|
||||
onNavigateBack()
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (countdown == null) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(stringResource(Res.string.countdown_not_found))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 剩余天数
|
||||
val daysRemaining = DateUtils.daysRemaining(countdown.targetDate)
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.White)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
AppHeader(
|
||||
title = Res.string.title_countdown_info,
|
||||
showBack = true,
|
||||
onBackClick = { onNavigateBack.invoke() },
|
||||
trailingIcon = Icons.Default.Edit,
|
||||
onTrailingClick = { onNavigateToEdit.invoke() }
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF5F5F5))
|
||||
.padding(16.dp)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(20.dp))
|
||||
.background(
|
||||
brush = androidx.compose.ui.graphics.Brush.linearGradient(
|
||||
colors = listOf(
|
||||
countdown.category.color.backgroundColor,
|
||||
Color.Transparent
|
||||
)
|
||||
)
|
||||
)
|
||||
.padding(20.dp)
|
||||
) {
|
||||
Column(modifier = Modifier.align(Alignment.CenterStart)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
text = countdown.title,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
color = Color(0xFF111111)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Chip(text = countdown.category.name)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.CalendarToday,
|
||||
contentDescription = null
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = countdown.targetDate.toString(),
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFF444444)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Column(
|
||||
modifier = Modifier.align(Alignment.TopEnd),
|
||||
horizontalAlignment = Alignment.End
|
||||
) {
|
||||
Text(
|
||||
text = daysRemaining.toString(),
|
||||
fontSize = 44.sp,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
color = Color(0xFF111111)
|
||||
)
|
||||
Text(
|
||||
text = stringResource(Res.string.label_days),
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color(0xFF666666)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
countdown.description.let {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = stringResource(Res.string.event_description),
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFF333333)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(Color.White)
|
||||
.padding(12.dp)
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
text = countdown.description,
|
||||
color = countdown.category.color.textColor,
|
||||
lineHeight = 20.sp
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = stringResource(Res.string.detail_information),
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFF333333)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
InfoItem(
|
||||
iconTint = Color(0xFF667EEA),
|
||||
text = "${stringResource(Res.string.reminder)}:${
|
||||
stringResource(countdown.notificationEnabled.displayNameRes)
|
||||
}",
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
InfoItem(
|
||||
iconTint = Color(0xFF999999),
|
||||
text = "${stringResource(Res.string.created_at)}:${countdown.createdAt}",
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun InfoItem(iconTint: Color, text: String, modifier: Modifier = Modifier) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.background(Color.White)
|
||||
.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(18.dp)
|
||||
.clip(RoundedCornerShape(6.dp))
|
||||
.background(iconTint)
|
||||
) {}
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Text(text = text, fontSize = 13.sp, color = Color(0xFF555555))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,274 @@
|
||||
package com.taskttl.presentation.countdown
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.DateRange
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.FilterChipDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.taskttl.data.local.model.Category
|
||||
import com.taskttl.data.local.model.Countdown
|
||||
import com.taskttl.data.local.model.ReminderFrequency
|
||||
import com.taskttl.data.state.CountdownEffect
|
||||
import com.taskttl.data.state.CountdownIntent
|
||||
import com.taskttl.data.viewmodel.CountdownViewModel
|
||||
import com.taskttl.ui.components.AppHeader
|
||||
import com.taskttl.ui.components.CategoryCard
|
||||
import com.taskttl.ui.components.CompactDatePickerDialog
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import taskttl.composeapp.generated.resources.Res
|
||||
import taskttl.composeapp.generated.resources.desc_select_date
|
||||
import taskttl.composeapp.generated.resources.label_countdown_description
|
||||
import taskttl.composeapp.generated.resources.label_countdown_title
|
||||
import taskttl.composeapp.generated.resources.label_notification_setting
|
||||
import taskttl.composeapp.generated.resources.label_select_category
|
||||
import taskttl.composeapp.generated.resources.label_target_date
|
||||
import taskttl.composeapp.generated.resources.title_add_countdown
|
||||
import taskttl.composeapp.generated.resources.title_edit_countdown
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.ExperimentalTime
|
||||
import kotlin.uuid.ExperimentalUuidApi
|
||||
import kotlin.uuid.Uuid
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalTime::class, ExperimentalUuidApi::class)
|
||||
@Composable
|
||||
fun CountdownEditScreen(
|
||||
countdownId: String? = null,
|
||||
onNavigateBack: () -> Unit,
|
||||
viewModel: CountdownViewModel = koinViewModel()
|
||||
) {
|
||||
LaunchedEffect(countdownId) {
|
||||
countdownId?.let { viewModel.handleIntent(CountdownIntent.GetCountdownById(it)) }
|
||||
}
|
||||
|
||||
val state by viewModel.state.collectAsState()
|
||||
val editingCountdown = state.editingCountdown
|
||||
|
||||
var showDatePicker by remember { mutableStateOf(false) }
|
||||
|
||||
var title by remember { mutableStateOf("") }
|
||||
var description by remember { mutableStateOf("") }
|
||||
var selectedCategory by remember { mutableStateOf<Category?>(null) }
|
||||
var targetDate by remember { mutableStateOf<LocalDateTime?>(null) }
|
||||
var notificationEnabled by remember { mutableStateOf(ReminderFrequency.OFF) }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.effects.collect { effect ->
|
||||
when (effect) {
|
||||
is CountdownEffect.NavigateBack -> {
|
||||
onNavigateBack.invoke()
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(editingCountdown) {
|
||||
editingCountdown?.let {
|
||||
title = it.title
|
||||
description = it.description
|
||||
selectedCategory = it.category
|
||||
targetDate = it.targetDate
|
||||
notificationEnabled = it.notificationEnabled
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
AppHeader(
|
||||
title = if (countdownId == null) Res.string.title_add_countdown else Res.string.title_edit_countdown,
|
||||
showBack = true,
|
||||
onBackClick = { onNavigateBack.invoke() },
|
||||
trailingIcon = if (countdownId == null) Icons.Default.Add else Icons.Default.Edit,
|
||||
onTrailingClick = {
|
||||
if (title.isNotBlank() && targetDate != null && selectedCategory != null) {
|
||||
val now = Clock.System.now()
|
||||
.toLocalDateTime(TimeZone.currentSystemDefault())
|
||||
val countdown = Countdown(
|
||||
id = editingCountdown?.id ?: Uuid.random().toString(),
|
||||
title = title.trim(),
|
||||
description = description.trim(),
|
||||
category = selectedCategory!!,
|
||||
targetDate = targetDate!!,
|
||||
createdAt = now,
|
||||
updatedAt = now,
|
||||
notificationEnabled = notificationEnabled
|
||||
)
|
||||
|
||||
if (countdownId == null) {
|
||||
viewModel.handleIntent(CountdownIntent.AddCountdown(countdown))
|
||||
} else {
|
||||
editingCountdown?.let { countdown.isActive = editingCountdown.isActive }
|
||||
viewModel.handleIntent(CountdownIntent.UpdateCountdown(countdown))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(16.dp),
|
||||
) {
|
||||
// 倒数日标题
|
||||
OutlinedTextField(
|
||||
value = title,
|
||||
onValueChange = { title = it },
|
||||
label = { Text(stringResource(Res.string.label_countdown_title)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// 倒数日描述
|
||||
OutlinedTextField(
|
||||
value = description,
|
||||
onValueChange = { description = it },
|
||||
label = { Text(stringResource(Res.string.label_countdown_description)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
minLines = 3,
|
||||
maxLines = 5
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// 分类选择
|
||||
Text(
|
||||
text = stringResource(Res.string.label_select_category),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(4),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.height(80.dp)
|
||||
) {
|
||||
items(state.categories) { category ->
|
||||
CategoryCard(
|
||||
category = category,
|
||||
isSelected = selectedCategory == category,
|
||||
onClick = { selectedCategory = category }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// 目标日期
|
||||
OutlinedTextField(
|
||||
value = targetDate?.toString() ?: "",
|
||||
onValueChange = { },
|
||||
label = { Text(stringResource(Res.string.label_target_date)) },
|
||||
modifier = Modifier.fillMaxWidth().clickable { showDatePicker = true },
|
||||
readOnly = true,
|
||||
trailingIcon = {
|
||||
IconButton(onClick = { showDatePicker = true }) {
|
||||
Icon(
|
||||
Icons.Default.DateRange,
|
||||
contentDescription = stringResource(Res.string.desc_select_date)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
CompactDatePickerDialog(
|
||||
show = showDatePicker,
|
||||
initialSelected = targetDate,
|
||||
onConfirm = { selected -> targetDate = selected },
|
||||
onDismiss = { showDatePicker = false }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 12.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(Res.string.label_notification_setting),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
ReminderFrequency.entries.forEach { frequency ->
|
||||
FilterChip(
|
||||
selected = notificationEnabled == frequency,
|
||||
onClick = { notificationEnabled = frequency },
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(frequency.displayNameRes),
|
||||
textAlign = TextAlign.Center,
|
||||
fontSize = 10.sp,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
colors = FilterChipDefaults.filterChipColors(
|
||||
selectedContainerColor = MaterialTheme.colorScheme.primary.copy(
|
||||
alpha = 0.2f
|
||||
),
|
||||
selectedLabelColor = MaterialTheme.colorScheme.primary
|
||||
),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,543 @@
|
||||
package com.taskttl.presentation.countdown
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.CalendarToday
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.FilterList
|
||||
import androidx.compose.material.icons.filled.MoreHoriz
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavHostController
|
||||
import com.taskttl.core.routes.Routes
|
||||
import com.taskttl.core.utils.DateUtils
|
||||
import com.taskttl.data.local.model.Countdown
|
||||
import com.taskttl.data.state.CountdownEffect
|
||||
import com.taskttl.data.state.CountdownIntent
|
||||
import com.taskttl.data.viewmodel.CountdownViewModel
|
||||
import com.taskttl.ui.components.AppHeader
|
||||
import com.taskttl.ui.components.CategoryFilter
|
||||
import org.jetbrains.compose.resources.stringResource
|
||||
import org.jetbrains.compose.ui.tooling.preview.Preview
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import taskttl.composeapp.generated.resources.Res
|
||||
import taskttl.composeapp.generated.resources.delete
|
||||
import taskttl.composeapp.generated.resources.desc_add_countdown
|
||||
import taskttl.composeapp.generated.resources.label_countdown_list
|
||||
import taskttl.composeapp.generated.resources.label_days
|
||||
import taskttl.composeapp.generated.resources.label_edit
|
||||
import taskttl.composeapp.generated.resources.text_add_countdown_tip
|
||||
import taskttl.composeapp.generated.resources.text_no_countdowns
|
||||
import taskttl.composeapp.generated.resources.title_countdown
|
||||
import taskttl.composeapp.generated.resources.title_edit_countdown
|
||||
|
||||
@Composable
|
||||
@Preview
|
||||
fun CountdownScreen(
|
||||
navController: NavHostController,
|
||||
viewModel: CountdownViewModel = koinViewModel()
|
||||
) {
|
||||
val state by viewModel.state.collectAsState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.effects.collect { effect ->
|
||||
when (effect) {
|
||||
is CountdownEffect.NavigateBack -> {
|
||||
navController.popBackStack()
|
||||
}
|
||||
|
||||
is CountdownEffect.NavigateToCountdownDetail -> {
|
||||
// onNavigateToCountdownDetail(effect.countdownId)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.error?.let { error ->
|
||||
LaunchedEffect(error) {
|
||||
viewModel.handleIntent(CountdownIntent.ClearError)
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color.White)
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
AppHeader(
|
||||
title = Res.string.title_countdown,
|
||||
trailingIcon = Icons.Default.FilterList
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(Color(0xFFF5F5F5))
|
||||
.padding(16.dp)
|
||||
) {
|
||||
// 分类筛选
|
||||
CategoryFilter(
|
||||
categories = state.categories,
|
||||
selectedCategory = state.selectedCategory,
|
||||
onCategorySelected = {
|
||||
viewModel.handleIntent(CountdownIntent.FilterByCategory(it))
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = "${stringResource(Res.string.label_countdown_list)} (${state.filteredCountdowns.size})",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
when {
|
||||
state.isLoading -> {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
|
||||
state.filteredCountdowns.isEmpty() -> {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Text(
|
||||
text = stringResource(Res.string.text_no_countdowns),
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(Res.string.text_add_countdown_tip),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(state.filteredCountdowns) { countdown ->
|
||||
CountdownCard(
|
||||
countdown = countdown,
|
||||
onCardClick = {
|
||||
navController.navigate(
|
||||
Routes.Main.Countdown.CountdownDetail(countdown.id)
|
||||
)
|
||||
},
|
||||
onEdit = {
|
||||
navController.navigate(
|
||||
Routes.Main.Countdown.EditCountdown(countdown.id)
|
||||
)
|
||||
},
|
||||
onDelete = {
|
||||
viewModel.handleIntent(
|
||||
CountdownIntent.DeleteCountdown(countdown.id)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 悬浮按钮
|
||||
FloatingActionButton(
|
||||
onClick = { navController.navigate(Routes.Main.Countdown.AddCountdown) },
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(20.dp),
|
||||
containerColor = Color(0xFF667EEA)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = stringResource(Res.string.desc_add_countdown)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CountdownCard(
|
||||
countdown: Countdown,
|
||||
onEdit: () -> Unit = {},
|
||||
onDelete: () -> Unit = {},
|
||||
onCardClick: () -> Unit = {}
|
||||
) {
|
||||
val countdownTime = DateUtils.calculateCountdownTime(countdown.targetDate)
|
||||
countdown.category
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.shadow(4.dp, RoundedCornerShape(12.dp))
|
||||
.background(Color.White, RoundedCornerShape(12.dp))
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = null
|
||||
) { onCardClick() }
|
||||
) {
|
||||
Column {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.height(4.dp)
|
||||
.background(countdown.category.color.backgroundColor)
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 6.dp, end = 6.dp, top = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = countdown.title,
|
||||
fontSize = 18.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color(0xFF1A1A1A)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.CalendarToday,
|
||||
contentDescription = null,
|
||||
tint = Color(0xFF666666),
|
||||
modifier = Modifier.size(14.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(
|
||||
text = countdown.targetDate.toString(),
|
||||
fontSize = 14.sp,
|
||||
color = Color(0xFF666666)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.End,
|
||||
modifier = Modifier.widthIn(min = 80.dp)
|
||||
) {
|
||||
Text(
|
||||
text = countdownTime.days.toString(),
|
||||
fontSize = 32.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = countdown.category.color.textColor
|
||||
)
|
||||
Text(
|
||||
text = stringResource(Res.string.label_days),
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = Color(0xFF999999)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 6.dp, end = 6.dp, bottom = 8.dp)
|
||||
) {
|
||||
if (countdown.description.isNotBlank()) {
|
||||
Spacer(modifier = Modifier.height(6.dp))
|
||||
Text(
|
||||
text = countdown.description,
|
||||
fontSize = 13.sp,
|
||||
color = Color(0xFF999999),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 6.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.background(
|
||||
countdown.category.color.backgroundColor,
|
||||
RoundedCornerShape(20.dp)
|
||||
).padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = countdown.category.icon.icon,
|
||||
contentDescription = null,
|
||||
tint = countdown.category.color.iconColor,
|
||||
modifier = Modifier.size(14.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(
|
||||
text = countdown.category.name,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = countdown.category.color.textColor
|
||||
)
|
||||
}
|
||||
|
||||
var showReminderDialog by remember { mutableStateOf(false) }
|
||||
var showMoreMenu by remember { mutableStateOf(false) }
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
// IconBut({ showReminderDialog = true }, Icons.Default.Notifications)
|
||||
// Spacer(modifier = Modifier.width(6.dp))
|
||||
Box {
|
||||
IconBut({ showMoreMenu = true }, Icons.Default.MoreHoriz)
|
||||
DropdownMenu(
|
||||
expanded = showMoreMenu,
|
||||
onDismissRequest = { showMoreMenu = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(Res.string.label_edit)) },
|
||||
onClick = {
|
||||
onEdit.invoke();
|
||||
showMoreMenu = false
|
||||
}
|
||||
)
|
||||
// DropdownMenuItem(
|
||||
// text = { Text("分享倒数日") },
|
||||
// onClick = {
|
||||
// // onShare();
|
||||
// showMoreMenu = false
|
||||
// }
|
||||
// )
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(Res.string.delete),
|
||||
color = Color.Red
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
onDelete.invoke();
|
||||
showMoreMenu = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showReminderDialog) {
|
||||
ReminderDialog(
|
||||
onDismiss = { showReminderDialog = false },
|
||||
onSave = { /* TODO: 保存提醒逻辑 */ }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun IconBut(onClick: () -> Unit = {}, icon: ImageVector) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(32.dp)
|
||||
.clip(CircleShape)
|
||||
.background(Color(0xFFF5F5F5))
|
||||
.clickable { onClick() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = Color(0xFF666666),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ReminderDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onSave: (String) -> Unit
|
||||
) {
|
||||
var isEnabled by remember { mutableStateOf(true) }
|
||||
var timeBefore by remember { mutableStateOf("1天前") }
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = { onDismiss() },
|
||||
title = { Text("提醒设置") },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text("启用提醒")
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Switch(checked = isEnabled, onCheckedChange = { isEnabled = it })
|
||||
}
|
||||
|
||||
if (isEnabled) {
|
||||
Text("提前提醒时间")
|
||||
Box {
|
||||
OutlinedButton(
|
||||
onClick = { expanded = true },
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(timeBefore)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false }
|
||||
) {
|
||||
listOf("当天", "1天前", "3天前", "1周前").forEach {
|
||||
DropdownMenuItem(
|
||||
text = { Text(it) },
|
||||
onClick = {
|
||||
timeBefore = it
|
||||
expanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onSave(timeBefore)
|
||||
onDismiss()
|
||||
}) {
|
||||
Text("保存")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { onDismiss() }) {
|
||||
Text("取消")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MoreActionsDialog(
|
||||
onDismiss: () -> Unit,
|
||||
onEdit: () -> Unit,
|
||||
onShare: () -> Unit,
|
||||
onDelete: () -> Unit
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { onDismiss() },
|
||||
title = { Text("更多操作") },
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
ActionItem("编辑倒数日", Icons.Default.Edit) {
|
||||
onEdit(); onDismiss()
|
||||
}
|
||||
ActionItem("分享倒数日", Icons.Default.Share) {
|
||||
onShare(); onDismiss()
|
||||
}
|
||||
ActionItem("删除倒数日", Icons.Default.Delete, danger = true) {
|
||||
onDelete(); onDismiss()
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = { onDismiss() }) {
|
||||
Text("关闭")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActionItem(
|
||||
text: String,
|
||||
icon: ImageVector,
|
||||
danger: Boolean = false,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.clickable { onClick() }
|
||||
.background(if (danger) Color(0xFFFFE6E6) else Color(0xFFF5F5F5))
|
||||
.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = if (danger) Color(0xFFE53E3E) else Color(0xFF555555)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
text = text,
|
||||
color = if (danger) Color(0xFFE53E3E) else Color(0xFF333333),
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user