This commit is contained in:
2025-10-08 18:08:15 +08:00
parent dc71bb19a9
commit 989e5be041
109 changed files with 10815 additions and 170 deletions

View File

@@ -0,0 +1,81 @@
package com.taskttl.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Icon
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.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.stringResource
import taskttl.composeapp.generated.resources.Res
import taskttl.composeapp.generated.resources.action
import taskttl.composeapp.generated.resources.back
/**
* 应用程序标题
* @param [title] 标题
* @param [showBack] 显示返回
* @param [onBackClick] 上返回点击
* @param [trailingIcon] 尾随图标
* @param [onTrailingClick] 尾随点击
*/
@Composable
fun AppHeader(
title: StringResource,
showBack: Boolean = false,
onBackClick: (() -> Unit)? = null,
trailingIcon: ImageVector? = null,
onTrailingClick: (() -> Unit)? = null
) {
Row(
modifier = Modifier
.fillMaxWidth()
.background(
Brush.linearGradient(
colors = listOf(Color(0xFF667EEA), Color(0xFF764BA2))
)
)
.padding(horizontal = 20.dp, vertical = 15.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
if (showBack && onBackClick != null) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(Res.string.back),
tint = Color.White,
modifier = Modifier.clickable { onBackClick() }
)
}
Text(
text = stringResource(title),
color = Color.White,
fontSize = 20.sp,
fontWeight = FontWeight.Bold
)
if (trailingIcon != null) {
Icon(
imageVector = trailingIcon,
contentDescription = stringResource(Res.string.action),
tint = Color.White,
modifier = Modifier.clickable { onTrailingClick?.invoke() }
)
}
}
}

View File

@@ -0,0 +1,79 @@
package com.taskttl.ui.components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
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.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
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.draw.clip
import androidx.compose.ui.unit.dp
import com.taskttl.data.local.model.Category
import org.jetbrains.compose.resources.stringResource
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CategoryCard(
category: Category,
isSelected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier
.fillMaxWidth()
.clickable { onClick.invoke() },
colors = CardDefaults.cardColors(
containerColor = if (isSelected) {
category.color.backgroundColor.copy(alpha = 0.1f)
} else {
MaterialTheme.colorScheme.surface
}
),
border = if (isSelected) BorderStroke(2.dp, category.color.backgroundColor) else null
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(4.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.size(30.dp)
.clip(RoundedCornerShape(4.dp))
.background(category.color.backgroundColor.copy(alpha = 0.2f)),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = category.icon.icon,
contentDescription = stringResource(category.icon.displayNameRes),
tint = category.color.iconColor,
modifier = Modifier.size(25.dp)
)
}
Spacer(modifier = Modifier.height(4.dp))
Text(
text = category.name,
style = MaterialTheme.typography.bodyMedium,
color = if (isSelected) category.color.textColor else MaterialTheme.colorScheme.onSurface
)
}
}
}

View File

@@ -0,0 +1,50 @@
package com.taskttl.ui.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.taskttl.data.local.model.Category
import org.jetbrains.compose.resources.stringResource
import taskttl.composeapp.generated.resources.Res
import taskttl.composeapp.generated.resources.all_text
@Composable
fun CategoryFilter(
categories: List<Category>,
selectedCategory: Category?,
onCategorySelected: (Category?) -> Unit,
modifier: Modifier = Modifier
) {
LazyRow(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
// 全部分类
item {
FilterChip(
selected = selectedCategory == null,
onClick = { onCategorySelected(null) },
label = { Text(stringResource(Res.string.all_text)) }
)
}
// 各个分类
items(categories) { category ->
FilterChip(
selected = selectedCategory == category,
onClick = { onCategorySelected(category) },
label = { Text(category.name, color = category.color.textColor) },
colors = FilterChipDefaults.filterChipColors(
selectedContainerColor = category.color.backgroundColor,
selectedLabelColor = category.color.backgroundColor
)
)
}
}
}

View File

@@ -0,0 +1,177 @@
package com.taskttl.ui.components
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.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.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProgressIndicatorDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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 com.taskttl.data.local.model.CategoryStatistics
import org.jetbrains.compose.resources.stringResource
import taskttl.composeapp.generated.resources.Res
import taskttl.composeapp.generated.resources.active
import taskttl.composeapp.generated.resources.completed
import taskttl.composeapp.generated.resources.completion_rate
import taskttl.composeapp.generated.resources.total_countdowns
import taskttl.composeapp.generated.resources.total_tasks
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CategoryStatisticsCard(
statistics: CategoryStatistics,
categoryColor: Long,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(32.dp)
.clip(CircleShape)
.background(Color(categoryColor))
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = statistics.categoryName,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium
)
}
Spacer(modifier = Modifier.height(16.dp))
// 任务统计
if (statistics.totalTasks > 0) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Column {
Text(
text = stringResource(Res.string.total_tasks),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = "${statistics.totalTasks}",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
}
Column {
Text(
text = stringResource(Res.string.completed),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = "${statistics.completedTasks}",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
)
}
Column {
Text(
text = stringResource(Res.string.completion_rate),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = "${(statistics.completionRate * 100).toInt()}%",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = if (statistics.completionRate >= 0.8f)
MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.onSurface
)
}
}
Spacer(modifier = Modifier.height(12.dp))
// 进度条
LinearProgressIndicator(
progress = { statistics.completionRate },
modifier = Modifier
.fillMaxWidth()
.height(8.dp)
.clip(RoundedCornerShape(4.dp)),
color = Color(categoryColor),
trackColor = ProgressIndicatorDefaults.linearTrackColor,
strokeCap = ProgressIndicatorDefaults.LinearStrokeCap,
)
}
// 倒数日统计
if (statistics.totalCountdowns > 0) {
Spacer(modifier = Modifier.height(16.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Column {
Text(
text = stringResource(Res.string.total_countdowns),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = "${statistics.totalCountdowns}",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium
)
}
Column {
Text(
text = stringResource(Res.string.active),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = "${statistics.activeCountdowns}",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.primary
)
}
}
}
}
}
}

View File

@@ -0,0 +1,65 @@
package com.taskttl.ui.components
import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.runtime.Composable
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import org.jetbrains.compose.resources.stringResource
import taskttl.composeapp.generated.resources.Res
import taskttl.composeapp.generated.resources.cancel
import taskttl.composeapp.generated.resources.confirm
import kotlin.time.Clock
import kotlin.time.ExperimentalTime
import kotlin.time.Instant
@OptIn(ExperimentalMaterial3Api::class, ExperimentalTime::class)
@Composable
fun CompactDatePickerDialog(
show: Boolean,
initialSelected: LocalDateTime?,
onConfirm: (LocalDateTime) -> Unit,
onDismiss: () -> Unit
) {
if (!show) return
val initialMillis = (
initialSelected?.toInstant(TimeZone.currentSystemDefault())
?: Clock.System.now()
).toEpochMilliseconds()
val state = rememberDatePickerState(initialSelectedDateMillis = initialMillis)
DatePickerDialog(
onDismissRequest = onDismiss,
confirmButton = {
TextButton(
onClick = {
val millis = state.selectedDateMillis
if (millis != null) {
val instant = Instant.fromEpochMilliseconds(millis)
val selected = instant.toLocalDateTime(TimeZone.currentSystemDefault())
onConfirm(selected)
}
onDismiss()
}
) { Text(stringResource(Res.string.confirm)) }
},
dismissButton = {
TextButton(onClick = onDismiss) { Text(stringResource(Res.string.cancel)) }
}
) {
DatePicker(
state = state,
headline = null,
title = null,
showModeToggle = false
)
}
}

View File

@@ -0,0 +1,51 @@
package com.taskttl.ui.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Clear
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import org.jetbrains.compose.resources.StringResource
import org.jetbrains.compose.resources.stringResource
import taskttl.composeapp.generated.resources.Res
import taskttl.composeapp.generated.resources.clear_text
import taskttl.composeapp.generated.resources.search
import taskttl.composeapp.generated.resources.search_placeholder
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SearchBar(
query: String,
onQueryChange: (String) -> Unit,
modifier: Modifier = Modifier,
placeholder: StringResource = Res.string.search_placeholder
) {
OutlinedTextField(
value = query,
onValueChange = onQueryChange,
modifier = modifier,
placeholder = { Text(stringResource(placeholder)) },
leadingIcon = {
Icon(
imageVector = Icons.Default.Search,
contentDescription = stringResource(Res.string.search)
)
},
trailingIcon = {
if (query.isNotEmpty()) {
IconButton(onClick = { onQueryChange("") }) {
Icon(
imageVector = Icons.Default.Clear,
contentDescription = stringResource(Res.string.clear_text)
)
}
}
},
singleLine = true
)
}

View File

@@ -0,0 +1,109 @@
package com.taskttl.ui.theme
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
/** 浅色方案 */
private val LightColorScheme = lightColorScheme(
primary = Color(0xFF6750A4),
onPrimary = Color(0xFFFFFFFF),
primaryContainer = Color(0xFFEADDFF),
onPrimaryContainer = Color(0xFF21005D),
secondary = Color(0xFF625B71),
onSecondary = Color(0xFFFFFFFF),
secondaryContainer = Color(0xFFE8DEF8),
onSecondaryContainer = Color(0xFF1D192B),
tertiary = Color(0xFF7D5260),
onTertiary = Color(0xFFFFFFFF),
tertiaryContainer = Color(0xFFFFD8E4),
onTertiaryContainer = Color(0xFF31111D),
error = Color(0xFFBA1A1A),
onError = Color(0xFFFFFFFF),
errorContainer = Color(0xFFFFDAD6),
onErrorContainer = Color(0xFF410002),
background = Color(0xFFFFFBFE),
onBackground = Color(0xFF1C1B1F),
surface = Color(0xFFFFFBFE),
onSurface = Color(0xFF1C1B1F),
surfaceVariant = Color(0xFFE7E0EC),
onSurfaceVariant = Color(0xFF49454F),
outline = Color(0xFF79747E),
outlineVariant = Color(0xFFCAC4D0),
scrim = Color(0xFF000000),
inverseSurface = Color(0xFF313033),
inverseOnSurface = Color(0xFFF4EFF4),
inversePrimary = Color(0xFFD0BCFF),
surfaceDim = Color(0xFFDDD8DD),
surfaceBright = Color(0xFFFFFBFE),
surfaceContainerLowest = Color(0xFFFFFFFF),
surfaceContainerLow = Color(0xFFF7F2FA),
surfaceContainer = Color(0xFFF1ECF4),
surfaceContainerHigh = Color(0xFFECE6F0),
surfaceContainerHighest = Color(0xFFE6E0E9)
)
/** 深色配色方案 */
private val DarkColorScheme = darkColorScheme(
primary = Color(0xFFD0BCFF),
onPrimary = Color(0xFF381E72),
primaryContainer = Color(0xFF4F378B),
onPrimaryContainer = Color(0xFFEADDFF),
secondary = Color(0xFFCCC2DC),
onSecondary = Color(0xFF332D41),
secondaryContainer = Color(0xFF4A4458),
onSecondaryContainer = Color(0xFFE8DEF8),
tertiary = Color(0xFFEFB8C8),
onTertiary = Color(0xFF492532),
tertiaryContainer = Color(0xFF633B48),
onTertiaryContainer = Color(0xFFFFD8E4),
error = Color(0xFFFFB4AB),
onError = Color(0xFF690005),
errorContainer = Color(0xFF93000A),
onErrorContainer = Color(0xFFFFDAD6),
background = Color(0xFF10131C),
onBackground = Color(0xFFE6E0E9),
surface = Color(0xFF10131C),
onSurface = Color(0xFFE6E0E9),
surfaceVariant = Color(0xFF49454F),
onSurfaceVariant = Color(0xFFCAC4D0),
outline = Color(0xFF938F99),
outlineVariant = Color(0xFF49454F),
scrim = Color(0xFF000000),
inverseSurface = Color(0xFFE6E0E9),
inverseOnSurface = Color(0xFF313033),
inversePrimary = Color(0xFF6750A4),
surfaceDim = Color(0xFF10131C),
surfaceBright = Color(0xFF383B42),
surfaceContainerLowest = Color(0xFF0B0E17),
surfaceContainerLow = Color(0xFF191C24),
surfaceContainer = Color(0xFF1D2028),
surfaceContainerHigh = Color(0xFF282A32),
surfaceContainerHighest = Color(0xFF33353D)
)
/**
* 应用主题
* @param [darkTheme] 黑暗主题
* @param [content] 内容
*/
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = if (darkTheme) {
DarkColorScheme
} else {
LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = mainTypography(),
content = content
)
}

View File

@@ -0,0 +1,40 @@
package com.taskttl.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.runtime.Composable
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
/**
* 主要排版
* @return [Typography]
*/
@Composable
fun mainTypography(): Typography {
return Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
),
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
)
}