feat: 支持多选和删除待办(后续优化逻辑)
This commit is contained in:
parent
1656063449
commit
366f19ca06
11 changed files with 256 additions and 41 deletions
|
|
@ -19,6 +19,14 @@ object Repository {
|
|||
}
|
||||
|
||||
suspend fun deleteTodo(toDo: TodoEntity) {
|
||||
toDoDao.delete(toDo.id)
|
||||
toDoDao.delete(toDo)
|
||||
}
|
||||
|
||||
suspend fun deleteTodoFromIds(toDoItems: List<Int>) {
|
||||
toDoDao.deleteFromIds(toDoItems.toSet())
|
||||
}
|
||||
|
||||
/*suspend fun deleteAllTodo() {
|
||||
toDoDao.deleteAllTodo()
|
||||
}*/
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
package cn.super12138.todo.logic.database
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
|
|
@ -18,6 +19,12 @@ interface TodoDao {
|
|||
@Update
|
||||
suspend fun update(toDo: TodoEntity)
|
||||
|
||||
@Query("DELETE FROM todo WHERE id = :toDoId")
|
||||
suspend fun delete(toDoId: Int)
|
||||
@Delete
|
||||
suspend fun delete(toDo: TodoEntity)
|
||||
|
||||
@Query("DELETE FROM todo WHERE id in (:toDoIds)")
|
||||
suspend fun deleteFromIds(toDoIds: Set<Int>)
|
||||
|
||||
/*@Query("DELETE FROM todo")
|
||||
suspend fun deleteAllTodo()*/
|
||||
}
|
||||
|
|
@ -10,7 +10,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cn.super12138.todo.ui.TodoDefaults
|
||||
|
||||
/**
|
||||
* 带动画且比 Material 3 内置组件动画好看的的可扩展 FAB
|
||||
|
|
@ -36,7 +35,7 @@ fun AnimatedExtendedFloatingActionButton(
|
|||
modifier = modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = TodoDefaults.screenPadding),
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
icon()
|
||||
|
|
|
|||
|
|
@ -1,18 +1,31 @@
|
|||
package cn.super12138.todo.ui.pages.main
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.expandIn
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkOut
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.SelectAll
|
||||
import androidx.compose.material.icons.outlined.Settings
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
|
|
@ -44,36 +57,100 @@ fun MainPage(viewModel: MainViewModel, modifier: Modifier = Modifier) {
|
|||
}
|
||||
}
|
||||
|
||||
val selectedEditTodoItem = viewModel.selectedEditTodoItem
|
||||
val selectedEditTodoItem = viewModel.selectedEditTodo
|
||||
val selectedTodoIds = viewModel.selectedTodoIds.collectAsState()
|
||||
var openBottomSheet by rememberSaveable { mutableStateOf(false) }
|
||||
val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
|
||||
|
||||
val isSelectedIdsEmpty by remember {
|
||||
derivedStateOf {
|
||||
selectedTodoIds.value.isEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
val animatedTopAppBarColors by animateColorAsState(
|
||||
targetValue = if (isSelectedIdsEmpty) MaterialTheme.colorScheme.surface else MaterialTheme.colorScheme.surfaceContainerHighest
|
||||
)
|
||||
|
||||
BackHandler(enabled = !isSelectedIdsEmpty) {
|
||||
// 当按下返回键(或进行返回操作)时清空选择
|
||||
viewModel.clearAllTodoSelection()
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
navigationIcon = {
|
||||
AnimatedVisibility(!isSelectedIdsEmpty) {
|
||||
IconButton(onClick = { viewModel.clearAllTodoSelection() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Close,
|
||||
contentDescription = stringResource(R.string.tip_clear_selected_items)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(stringResource(R.string.app_name))
|
||||
Text(
|
||||
text = if (isSelectedIdsEmpty) {
|
||||
stringResource(R.string.app_name)
|
||||
} else {
|
||||
stringResource(
|
||||
R.string.title_selected_count,
|
||||
selectedTodoIds.value.size
|
||||
)
|
||||
}
|
||||
)
|
||||
},
|
||||
actions = {
|
||||
IconButton(
|
||||
onClick = {}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Settings,
|
||||
contentDescription = stringResource(R.string.page_settings)
|
||||
)
|
||||
AnimatedContent(isSelectedIdsEmpty) { isEmpty ->
|
||||
if (isEmpty) {
|
||||
IconButton(
|
||||
onClick = {}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Settings,
|
||||
contentDescription = stringResource(R.string.page_settings)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Row {
|
||||
IconButton(onClick = { viewModel.toggleAllSelected() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.SelectAll,
|
||||
contentDescription = stringResource(R.string.tip_select_all)
|
||||
)
|
||||
}
|
||||
IconButton(onClick = { viewModel.deleteSelectedTodo() }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Delete,
|
||||
contentDescription = stringResource(R.string.action_delete)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
colors = TopAppBarDefaults.largeTopAppBarColors().copy(
|
||||
containerColor = animatedTopAppBarColors
|
||||
)
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
TodoFAB(
|
||||
expanded = isExpanded,
|
||||
onClick = { openBottomSheet = true }
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = isSelectedIdsEmpty,
|
||||
enter = fadeIn() + expandIn(),
|
||||
exit = shrinkOut() + fadeOut()
|
||||
) {
|
||||
TodoFAB(
|
||||
expanded = isExpanded,
|
||||
onClick = { openBottomSheet = true }
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = modifier
|
||||
) { innerPadding ->
|
||||
|
|
@ -91,8 +168,15 @@ fun MainPage(viewModel: MainViewModel, modifier: Modifier = Modifier) {
|
|||
state = listState,
|
||||
list = toDoList.value.filter { item -> !item.isCompleted },
|
||||
onItemClick = { item ->
|
||||
openBottomSheet = true
|
||||
viewModel.setEditTodoItem(item)
|
||||
if (isSelectedIdsEmpty) {
|
||||
openBottomSheet = true
|
||||
viewModel.setEditTodoItem(item)
|
||||
} else {
|
||||
viewModel.toggleTodoSelection(item)
|
||||
}
|
||||
},
|
||||
onItemLongClick = { item ->
|
||||
viewModel.toggleTodoSelection(item)
|
||||
},
|
||||
onItemChecked = { item ->
|
||||
item.apply {
|
||||
|
|
@ -107,6 +191,7 @@ fun MainPage(viewModel: MainViewModel, modifier: Modifier = Modifier) {
|
|||
viewModel.playConfetti()
|
||||
}
|
||||
},
|
||||
selectedTodoIds = selectedTodoIds.value,
|
||||
modifier = Modifier
|
||||
.weight(3f)
|
||||
.fillMaxSize()
|
||||
|
|
@ -126,8 +211,15 @@ fun MainPage(viewModel: MainViewModel, modifier: Modifier = Modifier) {
|
|||
state = listState,
|
||||
list = toDoList.value.filter { item -> !item.isCompleted },
|
||||
onItemClick = { item ->
|
||||
openBottomSheet = true
|
||||
viewModel.setEditTodoItem(item)
|
||||
if (isSelectedIdsEmpty) {
|
||||
openBottomSheet = true
|
||||
viewModel.setEditTodoItem(item)
|
||||
} else {
|
||||
viewModel.toggleTodoSelection(item)
|
||||
}
|
||||
},
|
||||
onItemLongClick = { item ->
|
||||
viewModel.toggleTodoSelection(item)
|
||||
},
|
||||
onItemChecked = { item ->
|
||||
item.apply {
|
||||
|
|
@ -142,6 +234,7 @@ fun MainPage(viewModel: MainViewModel, modifier: Modifier = Modifier) {
|
|||
viewModel.playConfetti()
|
||||
}
|
||||
},
|
||||
selectedTodoIds = selectedTodoIds.value,
|
||||
modifier = Modifier
|
||||
.weight(3f)
|
||||
.fillMaxSize()
|
||||
|
|
|
|||
|
|
@ -29,7 +29,9 @@ fun ManagerFragment(
|
|||
state: LazyListState,
|
||||
list: List<TodoEntity>,
|
||||
onItemClick: (TodoEntity) -> Unit = {},
|
||||
onItemLongClick: (TodoEntity) -> Unit = {},
|
||||
onItemChecked: (TodoEntity) -> Unit = {},
|
||||
selectedTodoIds: List<Int>,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
|
@ -67,10 +69,12 @@ fun ManagerFragment(
|
|||
content = item.content,
|
||||
subject = Subjects.fromId(item.subject).getDisplayName(context),
|
||||
onCardClick = { onItemClick(item) },
|
||||
onCardLongClick = { onItemLongClick(item) },
|
||||
onChecked = { onItemChecked(item) },
|
||||
selected = selectedTodoIds.contains(item.id),
|
||||
modifier = Modifier
|
||||
.padding(vertical = 5.dp)
|
||||
.animateItem()
|
||||
.animateItem() // TODO: 设置动画时间
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,19 @@
|
|||
package cn.super12138.todo.ui.pages.main.components
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.basicMarquee
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
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.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Check
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
|
|
@ -18,31 +24,53 @@ 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.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cn.super12138.todo.R
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun TodoCard(
|
||||
content: String,
|
||||
subject: String,
|
||||
onCardClick: () -> Unit = {},
|
||||
onCardLongClick: () -> Unit = {},
|
||||
onChecked: () -> Unit = {},
|
||||
selected: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
ElevatedCard(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(80.dp),
|
||||
onClick = onCardClick
|
||||
.height(80.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.combinedClickable(
|
||||
onClick = onCardClick,
|
||||
onLongClick = onCardLongClick
|
||||
)
|
||||
.padding(horizontal = 15.dp)
|
||||
) {
|
||||
AnimatedVisibility(selected) {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(end = 15.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.onSecondaryContainer)
|
||||
.padding(5.dp)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Check,
|
||||
tint = MaterialTheme.colorScheme.onSecondary,
|
||||
contentDescription = stringResource(R.string.tip_select_this)
|
||||
)
|
||||
}
|
||||
}
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
|
|
@ -63,12 +91,14 @@ fun TodoCard(
|
|||
)
|
||||
}
|
||||
|
||||
IconButton(onClick = onChecked) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Check,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
contentDescription = stringResource(R.string.tip_mark_completed)
|
||||
)
|
||||
AnimatedVisibility(!selected) {
|
||||
IconButton(onClick = onChecked) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Check,
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
contentDescription = stringResource(R.string.tip_mark_completed)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/*Box(
|
||||
|
|
|
|||
|
|
@ -8,14 +8,22 @@ import androidx.lifecycle.viewModelScope
|
|||
import cn.super12138.todo.logic.Repository
|
||||
import cn.super12138.todo.logic.database.TodoEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MainViewModel : ViewModel() {
|
||||
val toDos: Flow<List<TodoEntity>> = Repository.getAllTodos()
|
||||
var selectedEditTodoItem by mutableStateOf<TodoEntity?>(null)
|
||||
var selectedEditTodo by mutableStateOf<TodoEntity?>(null)
|
||||
private set
|
||||
val showConfetti = mutableStateOf(false)
|
||||
|
||||
// 多选逻辑参考:https://github.com/X1nto/Mauth
|
||||
private val _selectedTodoIds = MutableStateFlow(listOf<Int>())
|
||||
val selectedTodoIds = _selectedTodoIds.asStateFlow()
|
||||
|
||||
fun addTodo(toDo: TodoEntity) {
|
||||
viewModelScope.launch {
|
||||
Repository.insertTodo(toDo)
|
||||
|
|
@ -28,17 +36,75 @@ class MainViewModel : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
fun playConfetti() {
|
||||
showConfetti.value = true
|
||||
}
|
||||
|
||||
fun setEditTodoItem(toDo: TodoEntity?) {
|
||||
selectedEditTodoItem = toDo
|
||||
}
|
||||
|
||||
fun deleteTodo(toDo: TodoEntity) {
|
||||
viewModelScope.launch {
|
||||
Repository.deleteTodo(toDo)
|
||||
}
|
||||
}
|
||||
|
||||
/*fun deleteAllTodo() {
|
||||
viewModelScope.launch {
|
||||
Repository.deleteAllTodo()
|
||||
}
|
||||
}*/
|
||||
|
||||
fun setEditTodoItem(toDo: TodoEntity?) {
|
||||
selectedEditTodo = toDo
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换待办的选择状态
|
||||
*/
|
||||
fun toggleTodoSelection(toDo: TodoEntity) {
|
||||
_selectedTodoIds.update { idList ->
|
||||
if (idList.contains(toDo.id)) {
|
||||
// 若已经选择取消选择
|
||||
idList - toDo.id
|
||||
} else {
|
||||
// 若未选择添加到列表中,立即选中
|
||||
idList + toDo.id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换是否全选
|
||||
*/
|
||||
fun toggleAllSelected() {
|
||||
viewModelScope.launch {
|
||||
toDos.firstOrNull()?.let { todos ->
|
||||
val allIds = todos.map { it.id }
|
||||
_selectedTodoIds.update { currentSelectedIds ->
|
||||
if (currentSelectedIds.containsAll(allIds)) {
|
||||
// 如果当前是全选状态,取消所有选择(切换为全不选)
|
||||
emptyList()
|
||||
} else {
|
||||
// 如果当前不是全选状态,选中所有项
|
||||
allIds
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除全部已选择的待办
|
||||
*/
|
||||
fun clearAllTodoSelection() {
|
||||
_selectedTodoIds.update { emptyList() }
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除选择的待办
|
||||
*/
|
||||
fun deleteSelectedTodo() {
|
||||
viewModelScope.launch {
|
||||
Repository.deleteTodoFromIds(selectedTodoIds.value)
|
||||
clearAllTodoSelection()
|
||||
}
|
||||
}
|
||||
|
||||
fun playConfetti() {
|
||||
showConfetti.value = true
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.ToDo" parent="android:Theme.Material.NoActionBar">
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
|
|
|
|||
|
|
@ -26,4 +26,7 @@
|
|||
<string name="tip_no_crash_logs">没有日志传入</string>
|
||||
<string name="action_exit_app">退出应用</string>
|
||||
<string name="action_delete">删除</string>
|
||||
<string name="tip_clear_selected_items">清除已选择的项目</string>
|
||||
<string name="tip_select_all">全选</string>
|
||||
<string name="title_selected_count">已选择 %s 项</string>
|
||||
</resources>
|
||||
|
|
@ -25,4 +25,7 @@
|
|||
<string name="tip_no_crash_logs">No crash logs</string>
|
||||
<string name="action_exit_app">Exit app</string>
|
||||
<string name="action_delete">Delete</string>
|
||||
<string name="tip_clear_selected_items">Clear selected items</string>
|
||||
<string name="tip_select_all">Select all</string>
|
||||
<string name="title_selected_count">%s selected</string>
|
||||
</resources>
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="Theme.ToDo" parent="android:Theme.Material.Light.NoActionBar">
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
|
|
|
|||
Loading…
Reference in a new issue