diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5c5a929..cca1d33 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -38,7 +38,7 @@ android { applicationId = "cn.super12138.todo" minSdk = 24 targetSdk = 36 - versionCode = 991 + versionCode = 992 versionName = "2.3.3" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/components/EmptyTip.kt b/app/src/main/kotlin/cn/super12138/todo/ui/components/EmptyTip.kt new file mode 100644 index 0000000..6f727f0 --- /dev/null +++ b/app/src/main/kotlin/cn/super12138/todo/ui/components/EmptyTip.kt @@ -0,0 +1,53 @@ +package cn.super12138.todo.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +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.res.painterResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import cn.super12138.todo.R +import cn.super12138.todo.ui.TodoDefaults + +enum class EmptyTipType { + Search, + List, + TaskCompleted +} + +@Composable +fun EmptyTip( + modifier: Modifier = Modifier, + size: Dp = 48.dp, + type: EmptyTipType +) { + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .padding(TodoDefaults.screenHorizontalPadding) + .size(size) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.secondaryContainer) + ) { + Icon( + painter = painterResource( + id = when (type) { + EmptyTipType.List -> R.drawable.ic_list_no_item + EmptyTipType.Search -> R.drawable.ic_search_not_found + EmptyTipType.TaskCompleted -> R.drawable.ic_thumb_up + } + ), + contentDescription = null, + tint = MaterialTheme.colorScheme.secondary, + modifier = Modifier.size(size / 2) + ) + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/components/ScreenContainer.kt b/app/src/main/kotlin/cn/super12138/todo/ui/components/ScreenContainer.kt index 41e0e14..77b5539 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/components/ScreenContainer.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/components/ScreenContainer.kt @@ -2,7 +2,6 @@ package cn.super12138.todo.ui.components import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TopNavigation.kt b/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TopNavigation.kt index 3528696..ea354f6 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TopNavigation.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/navigation/TopNavigation.kt @@ -2,6 +2,7 @@ package cn.super12138.todo.ui.navigation import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.SharedTransitionLayout +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier @@ -21,12 +22,12 @@ import cn.super12138.todo.ui.pages.settings.SettingsDeveloperOptionsPadding import cn.super12138.todo.ui.pages.settings.SettingsInterface import cn.super12138.todo.ui.pages.settings.SettingsMain import cn.super12138.todo.ui.pages.tasks.TasksPage -import cn.super12138.todo.ui.theme.fadeThrough +import cn.super12138.todo.ui.theme.fadeScale import cn.super12138.todo.ui.theme.materialSharedAxisX import cn.super12138.todo.ui.theme.veilFade import cn.super12138.todo.ui.viewmodels.MainViewModel -@OptIn(ExperimentalAnimationApi::class) +@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterial3ExpressiveApi::class) @Composable fun TopNavigation( backStack: TopLevelBackStack, @@ -64,25 +65,18 @@ fun TopNavigation( ) } + val defaultTransition = fadeScale( + effectSpec = MaterialTheme.motionScheme.defaultEffectsSpec(), + spatialSpec = MaterialTheme.motionScheme.defaultSpatialSpec() + ) + SharedTransitionLayout { NavDisplay( backStack = backStack.backStack, onBack = ::onBack, - /*transitionSpec = { - fadeIn() togetherWith veilOut(targetColor = veilColor) - }, - popTransitionSpec = { - unveilIn(initialColor = veilColor) togetherWith fadeOut() - }, - predictivePopTransitionSpec = { - unveilIn(initialColor = veilColor) togetherWith fadeOut() - },*/ - /** - * 来自:https://github.com/material-components/material-components-android/blob/master/lib/java/com/google/android/material/transition/MaterialFadeThrough.java#L33 - */ - transitionSpec = { fadeThrough() }, - popTransitionSpec = { fadeThrough() }, - predictivePopTransitionSpec = { fadeThrough() }, + transitionSpec = { defaultTransition }, + popTransitionSpec = { defaultTransition }, + predictivePopTransitionSpec = { defaultTransition }, entryProvider = entryProvider { entry { OverviewPage(viewModel = viewModel) diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/overview/components/UpcomingTaskCard.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/overview/components/UpcomingTaskCard.kt index be85de2..6110f21 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/overview/components/UpcomingTaskCard.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/overview/components/UpcomingTaskCard.kt @@ -1,16 +1,19 @@ package cn.super12138.todo.ui.pages.overview.components +import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.layout.Arrangement 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.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Badge import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults @@ -23,12 +26,16 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import cn.super12138.todo.R import cn.super12138.todo.logic.database.TodoEntity import cn.super12138.todo.logic.model.Priority 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 import cn.super12138.todo.utils.containerColor import cn.super12138.todo.utils.toRelativeTimeString @@ -54,20 +61,43 @@ fun UpcomingTaskCard( text = stringResource(R.string.title_upcoming_task), style = MaterialTheme.typography.titleLarge ) - - LazyColumn( - verticalArrangement = Arrangement.spacedBy(8.dp) + val transitionSpec = fadeScale() + AnimatedContent( + targetState = nextWeekTodo.isEmpty(), + transitionSpec = { transitionSpec } ) { - items( - items = nextWeekTodo, - key = { it.id } - ) { - UpcomingTaskItem( - content = it.content, - category = it.category, - priority = Priority.fromFloat(it.priority), - dueDate = it.dueDate - ) + if (it) { + Column( + modifier = Modifier + .fillMaxSize() + .weight(1f) // 占满剩余空间 + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + EmptyTip(type = EmptyTipType.List) + + Text( + text = stringResource(R.string.tip_no_task_brief), + style = MaterialTheme.typography.labelLarge, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + } + } else { + LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) { + items( + items = nextWeekTodo, + key = { task -> task.id } + ) { task -> + UpcomingTaskItem( + content = task.content, + category = task.category, + priority = Priority.fromFloat(task.priority), + dueDate = task.dueDate + ) + } + } } } } diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsDataCategory.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsDataCategory.kt index 197b649..3d409e4 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsDataCategory.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/settings/SettingsDataCategory.kt @@ -1,10 +1,15 @@ package cn.super12138.todo.ui.pages.settings +import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.basicMarquee +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.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.FilledTonalIconButton @@ -23,19 +28,24 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp import cn.super12138.todo.R import cn.super12138.todo.logic.datastore.DataStoreManager +import cn.super12138.todo.ui.components.EmptyTip +import cn.super12138.todo.ui.components.EmptyTipType import cn.super12138.todo.ui.components.TodoFloatingActionButton import cn.super12138.todo.ui.components.TopAppBarScaffold import cn.super12138.todo.ui.pages.settings.components.SettingsContainer import cn.super12138.todo.ui.pages.settings.components.SettingsItem import cn.super12138.todo.ui.pages.settings.components.category.CategoryPromptDialog +import cn.super12138.todo.ui.theme.fadeScale import cn.super12138.todo.utils.VibrationUtils import kotlinx.coroutines.launch @@ -58,7 +68,8 @@ fun SettingsDataCategory( val isExpanded by remember { derivedStateOf { listState.firstVisibleItemIndex == 0 } } - // TODO: 取消5字分类限制 + val transitionSpec = fadeScale() + TopAppBarScaffold( title = stringResource(R.string.pref_category_category_management), onBack = onNavigateUp, @@ -76,56 +87,71 @@ fun SettingsDataCategory( }, modifier = modifier, ) { - SettingsContainer(Modifier.fillMaxSize()) { - if (categories.isEmpty()) { - item { + AnimatedContent( + targetState = categories.isEmpty(), + transitionSpec = { transitionSpec } + ) { + if (it) { + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + EmptyTip( + type = EmptyTipType.List, + size = 96.dp + ) + Text( text = stringResource(R.string.tip_no_category_page), - style = MaterialTheme.typography.bodyLarge, + style = MaterialTheme.typography.titleMedium, textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth() ) } } else { - // Keep stable content key (category) for animations, but compute rounding based on content - items( - items = categories, - key = { it } - ) { category -> - SettingsItem( - headlineContent = { - Text( - text = category, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.basicMarquee() - ) - }, - trailingContent = { - FilledTonalIconButton( - shapes = IconButtonDefaults.shapes(), - onClick = { - VibrationUtils.performHapticFeedback(view) - scope.launch { DataStoreManager.setCategories(categories - category) } - } - ) { - Icon( - painter = painterResource(R.drawable.ic_delete), - contentDescription = stringResource(R.string.action_delete) + SettingsContainer(Modifier.fillMaxSize()) { + items( + items = categories, + key = { category -> category } + ) { category -> + SettingsItem( + headlineContent = { + Text( + text = category, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.basicMarquee() ) - } - }, - onClick = { - initialCategory = category - showDialog = true - }, - modifier = Modifier.animateItem( - fadeInSpec = MaterialTheme.motionScheme.defaultEffectsSpec(), - placementSpec = MaterialTheme.motionScheme.defaultSpatialSpec(), - fadeOutSpec = MaterialTheme.motionScheme.defaultEffectsSpec() + }, + trailingContent = { + FilledTonalIconButton( + shapes = IconButtonDefaults.shapes(), + onClick = { + VibrationUtils.performHapticFeedback(view) + scope.launch { DataStoreManager.setCategories(categories - category) } + } + ) { + Icon( + painter = painterResource(R.drawable.ic_delete), + contentDescription = stringResource(R.string.action_delete) + ) + } + }, + onClick = { + initialCategory = category + showDialog = true + }, + modifier = Modifier.animateItem( + fadeInSpec = MaterialTheme.motionScheme.defaultEffectsSpec(), + placementSpec = MaterialTheme.motionScheme.defaultSpatialSpec(), + fadeOutSpec = MaterialTheme.motionScheme.defaultEffectsSpec() + ) ) - ) + } } } } diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/TasksPage.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/TasksPage.kt index 5eebec6..d2ba16f 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/TasksPage.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/TasksPage.kt @@ -1,6 +1,7 @@ package cn.super12138.todo.ui.pages.tasks import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.SharedTransitionScope import androidx.compose.animation.expandVertically @@ -16,8 +17,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -35,6 +36,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp import androidx.navigation3.ui.LocalNavAnimatedContentScope import cn.super12138.todo.R import cn.super12138.todo.constants.Constants @@ -42,11 +44,14 @@ import cn.super12138.todo.logic.database.TodoEntity import cn.super12138.todo.logic.model.Priority import cn.super12138.todo.ui.TodoDefaults import cn.super12138.todo.ui.components.ConfirmDialog +import cn.super12138.todo.ui.components.EmptyTip +import cn.super12138.todo.ui.components.EmptyTipType import cn.super12138.todo.ui.components.TodoFloatingActionButton import cn.super12138.todo.ui.components.TopAppBarScaffold import cn.super12138.todo.ui.pages.tasks.components.TodoCard import cn.super12138.todo.ui.pages.tasks.components.TodoSearchTextField import cn.super12138.todo.ui.pages.tasks.components.TodoTopAppBar +import cn.super12138.todo.ui.theme.fadeScale import cn.super12138.todo.ui.viewmodels.MainViewModel import cn.super12138.todo.utils.toLocalDateString @@ -64,30 +69,27 @@ fun SharedTransitionScope.TasksPage( val selectedTodos = viewModel.selectedTodoIds.collectAsState() // 状态持久化 - val searchFieldState = rememberTextFieldState() - val searchMode = rememberSaveable { mutableStateOf(false) } - - val listState = rememberLazyListState() + val searchFieldState = viewModel.searchFieldState var showDeleteConfirmDialog by rememberSaveable { mutableStateOf(false) } val selectedTodoIds by remember { derivedStateOf { selectedTodos.value } } val inSelectedMode by remember { derivedStateOf { !selectedTodoIds.isEmpty() } } - val expandedFab by remember { derivedStateOf { listState.firstVisibleItemIndex == 0 } } + val expandedFab by remember { derivedStateOf { viewModel.toDoListState.firstVisibleItemIndex == 0 } } - val filteredTodoList = if (searchFieldState.text.isBlank()) toDoList else toDoList.filter { + val filteredTodoList = if (viewModel.searchMode.value) toDoList.filter { it.content.contains(searchFieldState.text, ignoreCase = true) || it.category.contains(searchFieldState.text, ignoreCase = true) || it.dueDate?.toLocalDateString() ?.contains(searchFieldState.text, ignoreCase = true) == true - } - // TODO: 移除已完成任务的过滤及其设置项 - // if (showCompleted) toDoList else toDoList.filter { item -> !item.isCompleted } + } else toDoList + + val transitionSpec = fadeScale() // 当按下返回键(或进行返回操作)时清空选择,仅在非选择模式下生效 BackHandler(inSelectedMode) { viewModel.clearAllTodoSelection() } // 选择时自动退出搜索模式 - LaunchedEffect(inSelectedMode) { searchMode.value = false } + LaunchedEffect(inSelectedMode) { if (inSelectedMode) viewModel.searchMode.value = false } TopAppBarScaffold( topBar = { @@ -97,8 +99,8 @@ fun SharedTransitionScope.TasksPage( onCancelSelect = { viewModel.clearAllTodoSelection() }, onSelectAll = { viewModel.selectAllTodos() }, onDeleteSelectedTodo = { showDeleteConfirmDialog = true }, - onSearchModeChange = { searchMode.value = it }, - searchMode = searchMode.value, + onSearchModeChange = { viewModel.searchMode.value = it }, + searchMode = viewModel.searchMode.value, ) }, floatingActionButton = { @@ -124,8 +126,7 @@ fun SharedTransitionScope.TasksPage( ) { Column { AnimatedVisibility( - visible = searchMode.value, - + visible = viewModel.searchMode.value, enter = fadeIn(MaterialTheme.motionScheme.fastEffectsSpec()) + expandVertically( MaterialTheme.motionScheme.fastSpatialSpec() ), @@ -134,80 +135,97 @@ fun SharedTransitionScope.TasksPage( ), ) { TodoSearchTextField( - searchMode = searchMode.value, - onSearchModeChange = { searchMode.value = it }, + searchMode = viewModel.searchMode.value, + onSearchModeChange = { viewModel.searchMode.value = it }, textFieldState = searchFieldState ) } - - LazyColumn( - state = listState, - verticalArrangement = Arrangement.spacedBy(TodoDefaults.settingsItemPadding), - modifier = Modifier.fillMaxSize() + AnimatedContent( + targetState = filteredTodoList.isEmpty(), + transitionSpec = { transitionSpec } ) { - item { - Spacer(modifier = Modifier.size(TodoDefaults.screenVerticalPadding)) - } + if (it) { + Column( + modifier = Modifier + .fillMaxSize() + .weight(1f) // 占满剩余空间 + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + EmptyTip( + type = if (viewModel.searchMode.value) EmptyTipType.Search else EmptyTipType.TaskCompleted, + size = 96.dp + ) - if (filteredTodoList.isEmpty()) { - item { Text( - text = stringResource(R.string.tip_no_task), + text = stringResource(if (viewModel.searchMode.value) R.string.tip_search_task_not_found else R.string.tip_no_task), style = MaterialTheme.typography.titleMedium, textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth() ) } } else { - items( - items = filteredTodoList, - key = { it.id } - ) { task -> - TodoCard( - // id = item.id, - content = task.content, - category = task.category, - completed = task.isCompleted, - dueDate = task.dueDate, - priority = Priority.fromFloat(task.priority), - selected = selectedTodoIds.contains(task.id), - onCardClick = { - if (inSelectedMode) { - viewModel.toggleTodoSelection(task) - } else { - toTodoEditPage(task) - } - }, - onCardLongClick = { viewModel.toggleTodoSelection(task) }, - onChecked = { - viewModel.updateTodo(task.copy(isCompleted = true)) - viewModel.playConfetti() - }, - modifier = Modifier - .sharedBounds( - sharedContentState = rememberSharedContentState(key = "${Constants.KEY_TODO_ITEM_TRANSITION}_${task.id}"), - animatedVisibilityScope = LocalNavAnimatedContentScope.current, - resizeMode = SharedTransitionScope.ResizeMode.RemeasureToBounds - ) - .animateItem( - fadeInSpec = MaterialTheme.motionScheme.defaultEffectsSpec(), - placementSpec = MaterialTheme.motionScheme.defaultSpatialSpec(), - fadeOutSpec = MaterialTheme.motionScheme.fastEffectsSpec() - ) - ) - } - item { - Spacer(modifier = Modifier.size(TodoDefaults.screenVerticalPadding)) + LazyColumn( + state = viewModel.toDoListState, + verticalArrangement = Arrangement.spacedBy(TodoDefaults.settingsItemPadding), + modifier = Modifier.fillMaxSize() + ) { + item { + Spacer(modifier = Modifier.size(TodoDefaults.screenVerticalPadding)) + } + + items( + items = filteredTodoList, + key = { task -> task.id } + ) { task -> + TodoCard( + // id = item.id, + content = task.content, + category = task.category, + completed = task.isCompleted, + dueDate = task.dueDate, + priority = Priority.fromFloat(task.priority), + selected = selectedTodoIds.contains(task.id), + onCardClick = { + if (inSelectedMode) { + viewModel.toggleTodoSelection(task) + } else { + toTodoEditPage(task) + } + }, + onCardLongClick = { viewModel.toggleTodoSelection(task) }, + onChecked = { + viewModel.updateTodo(task.copy(isCompleted = true)) + viewModel.playConfetti() + }, + modifier = Modifier + .sharedBounds( + sharedContentState = rememberSharedContentState(key = "${Constants.KEY_TODO_ITEM_TRANSITION}_${task.id}"), + animatedVisibilityScope = LocalNavAnimatedContentScope.current, + resizeMode = SharedTransitionScope.ResizeMode.RemeasureToBounds + ) + .animateItem( + fadeInSpec = MaterialTheme.motionScheme.defaultEffectsSpec(), + placementSpec = MaterialTheme.motionScheme.defaultSpatialSpec(), + fadeOutSpec = MaterialTheme.motionScheme.fastEffectsSpec() + ) + ) + } + + item { + Spacer(modifier = Modifier.size(TodoDefaults.screenVerticalPadding)) + } } } } } + ConfirmDialog( + visible = showDeleteConfirmDialog, + iconRes = R.drawable.ic_delete, + text = stringResource(R.string.tip_delete_task, selectedTodoIds.size), + onConfirm = { viewModel.deleteSelectedTodo() }, + onDismiss = { showDeleteConfirmDialog = false } + ) } - ConfirmDialog( - visible = showDeleteConfirmDialog, - iconRes = R.drawable.ic_delete, - text = stringResource(R.string.tip_delete_task, selectedTodoIds.size), - onConfirm = { viewModel.deleteSelectedTodo() }, - onDismiss = { showDeleteConfirmDialog = false } - ) } \ No newline at end of file diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/components/TodoCard.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/components/TodoCard.kt index a8e7f77..267ac3d 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/components/TodoCard.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/components/TodoCard.kt @@ -75,7 +75,6 @@ fun TodoCard( ) { val view = LocalView.current val context = LocalContext.current - // TODO: 滑动删除 val cardColors = CardDefaults.cardColors(containerColor = TodoDefaults.Colors.Container) val animatedContainerColor by animateColorAsState(targetValue = if (selected) MaterialTheme.colorScheme.secondaryContainer else if (completed) cardColors.disabledContainerColor else cardColors.containerColor) diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/components/TodoSearchTextField.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/components/TodoSearchTextField.kt index ac96231..29e9e56 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/components/TodoSearchTextField.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/components/TodoSearchTextField.kt @@ -6,7 +6,6 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.input.TextFieldLineLimits import androidx.compose.foundation.text.input.TextFieldState @@ -24,7 +23,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalView 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.utils.VibrationUtils @@ -51,9 +49,9 @@ fun TodoSearchTextField( leadingIcon = { IconButton( onClick = { - VibrationUtils.performHapticFeedback(view) - onSearchModeChange(!searchMode) - } + VibrationUtils.performHapticFeedback(view) + onSearchModeChange(!searchMode) + } ) { Icon( painter = painterResource(R.drawable.ic_arrow_back), diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/components/TodoTopAppBar.kt b/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/components/TodoTopAppBar.kt index 19915e3..3ff01f5 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/components/TodoTopAppBar.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/pages/tasks/components/TodoTopAppBar.kt @@ -2,9 +2,7 @@ package cn.super12138.todo.ui.pages.tasks.components import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.AnimatedContentScope import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.ContentTransform import androidx.compose.animation.animateColorAsState import androidx.compose.animation.expandIn import androidx.compose.animation.fadeIn @@ -37,6 +35,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import cn.super12138.todo.R import cn.super12138.todo.ui.TodoDefaults +import cn.super12138.todo.ui.theme.fadeScale import cn.super12138.todo.utils.VibrationUtils @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @@ -57,22 +56,15 @@ fun TodoTopAppBar( animationSpec = MaterialTheme.motionScheme.fastSpatialSpec(), expandFrom = Alignment.CenterStart ) + val navIconExitTransition = fadeOut( animationSpec = MaterialTheme.motionScheme.fastEffectsSpec() ) + shrinkOut( animationSpec = MaterialTheme.motionScheme.fastSpatialSpec(), shrinkTowards = Alignment.CenterStart ) - val enterTransition = fadeIn(MaterialTheme.motionScheme.fastEffectsSpec()) + - scaleIn( - animationSpec = MaterialTheme.motionScheme.fastSpatialSpec(), - initialScale = 0.92f - ) - val exitTransition = fadeOut(MaterialTheme.motionScheme.fastEffectsSpec()) - val titleTransition = ContentTransform( - targetContentEnter = enterTransition, - initialContentExit = exitTransition - ) + + val defaultTransitionSpec = fadeScale() val view = LocalView.current val animatedContainerColor by animateColorAsState( @@ -108,7 +100,7 @@ fun TodoTopAppBar( title = { AnimatedContent( targetState = !selectedMode, - transitionSpec = { titleTransition } + transitionSpec = { defaultTransitionSpec } ) { if (it) { Text( @@ -131,7 +123,7 @@ fun TodoTopAppBar( actions = { AnimatedContent( targetState = selectedMode, - transitionSpec = { titleTransition } + transitionSpec = { defaultTransitionSpec } ) { if (it) { ActionMultipleSelection( @@ -155,7 +147,7 @@ fun TodoTopAppBar( @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable -private fun AnimatedContentScope.ActionSearch( +private fun ActionSearch( searchMode: Boolean, onSearchModeChange: (Boolean) -> Unit, modifier: Modifier = Modifier @@ -166,8 +158,8 @@ private fun AnimatedContentScope.ActionSearch( AnimatedVisibility( visible = !searchMode, - enter = fadeIn() + scaleIn(), - exit = fadeOut() + scaleOut(), + enter = fadeIn(MaterialTheme.motionScheme.fastEffectsSpec()) + scaleIn(MaterialTheme.motionScheme.fastSpatialSpec()), + exit = fadeOut(MaterialTheme.motionScheme.fastEffectsSpec()) + scaleOut(MaterialTheme.motionScheme.fastSpatialSpec()), ) { IconButton( shapes = IconButtonDefaults.shapes(), @@ -187,7 +179,7 @@ private fun AnimatedContentScope.ActionSearch( @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable -fun AnimatedContentScope.ActionMultipleSelection( +fun ActionMultipleSelection( onSelectAll: () -> Unit, onDeleteSelectedTodo: () -> Unit, modifier: Modifier = Modifier diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/theme/Motion.kt b/app/src/main/kotlin/cn/super12138/todo/ui/theme/Motion.kt index ac01bb9..97ba38f 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/theme/Motion.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/theme/Motion.kt @@ -6,6 +6,7 @@ import androidx.compose.animation.ExitTransition import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.FastOutLinearInEasing import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.FiniteAnimationSpec import androidx.compose.animation.core.LinearOutSlowInEasing import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn @@ -15,6 +16,9 @@ import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.unveilIn import androidx.compose.animation.veilOut +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color /** @@ -93,7 +97,7 @@ fun materialSharedAxisXOut( ) ) -fun fadeThrough( +/*fun fadeThrough( durationMillis: Int = 200, ): ContentTransform = ContentTransform( fadeThroughIn(durationMillis = durationMillis), @@ -118,7 +122,7 @@ fun fadeThroughOut( durationMillis = durationMillis, easing = FastOutLinearInEasing ) -) +)*/ fun veilFade( initialColor: Color, @@ -151,4 +155,34 @@ fun veilFadeOut( ): ExitTransition = fadeOut() + veilOut( targetColor = initialColor - ) \ No newline at end of file + ) + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun fadeScaleIn( + effectSpec: FiniteAnimationSpec = MaterialTheme.motionScheme.fastEffectsSpec(), + spatialSpec: FiniteAnimationSpec = MaterialTheme.motionScheme.fastSpatialSpec() +): EnterTransition = fadeIn(effectSpec) + + scaleIn( + animationSpec = spatialSpec, + initialScale = 0.92f + ) + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun fadeScaleOut( + effectSpec: FiniteAnimationSpec = MaterialTheme.motionScheme.fastEffectsSpec() +): ExitTransition = fadeOut(effectSpec) + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun fadeScale( + effectSpec: FiniteAnimationSpec = MaterialTheme.motionScheme.fastEffectsSpec(), + spatialSpec: FiniteAnimationSpec = MaterialTheme.motionScheme.fastSpatialSpec() +): ContentTransform = ContentTransform( + targetContentEnter = fadeScaleIn( + effectSpec = effectSpec, + spatialSpec = spatialSpec + ), + initialContentExit = fadeScaleOut(effectSpec = effectSpec) +) diff --git a/app/src/main/kotlin/cn/super12138/todo/ui/viewmodels/MainViewModel.kt b/app/src/main/kotlin/cn/super12138/todo/ui/viewmodels/MainViewModel.kt index 7155a90..f8ae3a3 100644 --- a/app/src/main/kotlin/cn/super12138/todo/ui/viewmodels/MainViewModel.kt +++ b/app/src/main/kotlin/cn/super12138/todo/ui/viewmodels/MainViewModel.kt @@ -2,6 +2,8 @@ package cn.super12138.todo.ui.viewmodels import android.content.Context import android.net.Uri +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope @@ -19,10 +21,13 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -39,8 +44,8 @@ class MainViewModel : ViewModel() { // 待办 private val toDos: Flow> = Repository.getAllTodos() - val sortedTodos: Flow> = - DataStoreManager.sortingMethodFlow.flatMapLatest { sortingMethod -> + val sortedTodos: StateFlow> = DataStoreManager.sortingMethodFlow + .flatMapLatest { sortingMethod -> toDos.map { list -> when (SortingMethod.fromId(sortingMethod)) { SortingMethod.Sequential -> list.sortedWith( @@ -88,9 +93,18 @@ class MainViewModel : ViewModel() { } } } + .stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5000), + initialValue = emptyList() + ) val showConfetti = mutableStateOf(false) + val toDoListState = LazyListState() + val searchMode = mutableStateOf(false) + val searchFieldState = TextFieldState() + // 多选逻辑参考:https://github.com/X1nto/Mauth private val _selectedTodoIds = MutableStateFlow(listOf()) val selectedTodoIds = _selectedTodoIds.asStateFlow() diff --git a/app/src/main/kotlin/cn/super12138/todo/utils/TodoExt.kt b/app/src/main/kotlin/cn/super12138/todo/utils/TodoExt.kt index 77bac31..6df05a1 100644 --- a/app/src/main/kotlin/cn/super12138/todo/utils/TodoExt.kt +++ b/app/src/main/kotlin/cn/super12138/todo/utils/TodoExt.kt @@ -2,11 +2,8 @@ package cn.super12138.todo.utils import android.content.Context import androidx.compose.foundation.shape.CornerBasedShape -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.Stable import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size @@ -14,7 +11,6 @@ import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.ContentDrawScope -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.Dp import cn.super12138.todo.R import cn.super12138.todo.logic.model.Priority @@ -139,7 +135,9 @@ fun Long?.toRelativeTimeString(context: Context): String { } @Composable -fun disabledContentColor(alpha: Float = 0.38f): Color = MaterialTheme.colorScheme.onSurface.copy(alpha = alpha) +fun disabledContentColor(alpha: Float = 0.38f): Color = + MaterialTheme.colorScheme.onSurface.copy(alpha = alpha) @Composable -fun disabledContainerColor(alpha: Float = 0.12f): Color = MaterialTheme.colorScheme.onSurface.copy(alpha = alpha) \ No newline at end of file +fun disabledContainerColor(alpha: Float = 0.12f): Color = + MaterialTheme.colorScheme.onSurface.copy(alpha = alpha) \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_list_no_item.xml b/app/src/main/res/drawable/ic_list_no_item.xml new file mode 100644 index 0000000..d96a767 --- /dev/null +++ b/app/src/main/res/drawable/ic_list_no_item.xml @@ -0,0 +1,14 @@ + + + + diff --git a/app/src/main/res/drawable/ic_search_not_found.xml b/app/src/main/res/drawable/ic_search_not_found.xml new file mode 100644 index 0000000..cd19778 --- /dev/null +++ b/app/src/main/res/drawable/ic_search_not_found.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_thumb_up.xml b/app/src/main/res/drawable/ic_thumb_up.xml new file mode 100644 index 0000000..a5f8ce9 --- /dev/null +++ b/app/src/main/res/drawable/ic_thumb_up.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index dd372be..a8d1145 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -108,7 +108,6 @@ أدخل الفئة التي تريد إضافتها لا توجد فئات مخصصة حاليًا. يمكنك إضافتها من الإعدادات. - لا توجد فئات مخصصة في الوقت الحالي. الفئة الافتراضية، يرجى تعديلها تخصيص خيارات المطور diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 7cbd006..a4042e8 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -107,7 +107,7 @@ 输入你想添加的分类名称 当前暂无自定义分类,你可以在设置中添加分类 - 当前暂无自定义分类 + 暂无自定义分类 默认分类,请修改 自定义 开发者选项 @@ -132,4 +132,6 @@ 搜索 关闭 截止日期 + 暂未搜索到内容 + 暂无任务 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index aa6eddd..eaa5875 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -108,7 +108,7 @@ Enter the category you want to add There are currently no custom categories. You can add categories in the settings. - There are no custom categories at the moment. + No custom categories Default Category, please modify Customization Developer Options @@ -133,4 +133,6 @@ Search Close Due date + No tasks found + No tasks \ No newline at end of file