Add SubtaskRow composable

pull/1952/head
Alex Baker 2 years ago
parent 1cac090c9d
commit ccaed6ddb4

@ -19,7 +19,7 @@ import javax.inject.Inject
class TagsControlSet : TaskEditControlFragment() {
@Inject lateinit var chipProvider: ChipProvider
override fun onRowClick() {
private fun onRowClick() {
val intent = Intent(context, TagPickerActivity::class.java)
intent.putParcelableArrayListExtra(TagPickerActivity.EXTRA_SELECTED, viewModel.selectedTags.value)
startActivityForResult(intent, REQUEST_TAG_PICKER_ACTIVITY)

@ -55,7 +55,7 @@ class TimerControlSet : TaskEditControlFragment() {
callback = activity as TimerControlSetCallback
}
override fun onRowClick() {
private fun onRowClick() {
if (dialog == null) {
dialog = buildDialog()
}

@ -0,0 +1,262 @@
package org.tasks.compose.edit
import android.content.res.Configuration
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.ContentAlpha
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.google.android.material.composethemeadapter.MdcTheme
import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.api.GtasksFilter
import com.todoroo.astrid.data.Task
import org.tasks.compose.*
import org.tasks.data.GoogleTask
import org.tasks.data.TaskContainer
@Composable
fun SubtaskRow(
filter: Filter?,
googleTask: GoogleTask?,
desaturate: Boolean,
existingSubtasks: List<TaskContainer>,
newSubtasks: List<Task>,
openSubtask: (Task) -> Unit,
completeExistingSubtask: (Task, Boolean) -> Unit,
completeNewSubtask: (Task) -> Unit,
toggleSubtask: (Long, Boolean) -> Unit,
addSubtask: () -> Unit,
deleteSubtask: (Task) -> Unit,
) {
TaskEditRow(
icon = {
TaskEditIcon(
id = org.tasks.R.drawable.ic_subdirectory_arrow_right_black_24dp,
modifier = Modifier
.padding(
start = 16.dp,
top = 20.dp,
end = 20.dp,
bottom = 20.dp
)
.alpha(ContentAlpha.medium),
)
},
content = {
Column {
val isGoogleTaskChild =
filter is GtasksFilter && googleTask != null && googleTask.parent > 0 && googleTask.listId == filter.remoteId
if (isGoogleTaskChild) {
DisabledText(
text = stringResource(id = org.tasks.R.string.subtasks_multilevel_google_task),
modifier = Modifier.padding(top = 20.dp, bottom = 20.dp, end = 16.dp)
)
} else {
Spacer(modifier = Modifier.height(height = 8.dp))
existingSubtasks.forEach { task ->
ExistingSubtaskRow(
task = task,
desaturate = desaturate,
indent = if (filter !is GtasksFilter) task.indent else 0,
onRowClick = { openSubtask(task.task) },
onCompleteClick = { completeExistingSubtask(task.task, !task.isCompleted) },
onToggleSubtaskClick = { toggleSubtask(task.id, !task.isCollapsed) }
)
}
newSubtasks.forEach { subtask ->
NewSubtaskRow(
subtask = subtask,
desaturate = desaturate,
addSubtask = addSubtask,
onComplete = completeNewSubtask,
onDelete = deleteSubtask,
)
}
DisabledText(
text = stringResource(id = org.tasks.R.string.TEA_add_subtask),
modifier = Modifier
.clickable { addSubtask() }
.padding(12.dp)
)
Spacer(modifier = Modifier.height(8.dp))
}
}
},
)
}
@Composable
fun NewSubtaskRow(
subtask: Task,
desaturate: Boolean,
addSubtask: () -> Unit,
onComplete: (Task) -> Unit,
onDelete: (Task) -> Unit,
) {
Row(verticalAlignment = Alignment.CenterVertically) {
CheckBox(
task = subtask,
onCompleteClick = { onComplete(subtask) },
modifier = Modifier.align(Alignment.Top),
desaturate = desaturate,
)
var text by remember { mutableStateOf(subtask.title ?: "") }
val focusRequester = remember { FocusRequester() }
BasicTextField(
value = text,
onValueChange = {
text = it
subtask.title = it
},
cursorBrush = SolidColor(MaterialTheme.colors.onSurface),
modifier = Modifier
.weight(1f)
.focusable(enabled = true)
.focusRequester(focusRequester)
.alpha(if (subtask.isCompleted) ContentAlpha.disabled else ContentAlpha.high),
textStyle = MaterialTheme.typography.body1.copy(
textDecoration = if (subtask.isCompleted) TextDecoration.LineThrough else TextDecoration.None,
color = MaterialTheme.colors.onSurface,
),
keyboardOptions = KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences,
imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(
onDone = {
if (text.isNotBlank()) {
addSubtask()
}
}
),
singleLine = true,
maxLines = Int.MAX_VALUE,
)
ClearButton { onDelete(subtask) }
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
}
@Composable
fun ExistingSubtaskRow(
task: TaskContainer, indent: Int,
desaturate: Boolean,
onRowClick: () -> Unit,
onCompleteClick: () -> Unit,
onToggleSubtaskClick: () -> Unit,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable { onRowClick() }
.padding(end = 16.dp)
) {
Spacer(modifier = Modifier.width((indent * 20).dp))
CheckBox(
task = task.task,
onCompleteClick = onCompleteClick,
desaturate = desaturate
)
Text(
text = task.title,
modifier = Modifier
.weight(1f)
.alpha(if (task.isCompleted || task.isHidden) ContentAlpha.disabled else ContentAlpha.high),
style = MaterialTheme.typography.body1.copy(
textDecoration = if (task.isCompleted) TextDecoration.LineThrough else TextDecoration.None
)
)
if (task.hasChildren()) {
SubtaskChip(
task = task,
compact = true,
onClick = onToggleSubtaskClick,
)
}
}
}
@Preview(showBackground = true, widthDp = 320)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 320)
@Composable
fun NoSubtasks() {
MdcTheme {
SubtaskRow(
filter = null,
googleTask = null,
desaturate = true,
existingSubtasks = emptyList(),
newSubtasks = emptyList(),
openSubtask = {},
completeExistingSubtask = { _, _ -> },
completeNewSubtask = {},
toggleSubtask = { _, _ -> },
addSubtask = {},
deleteSubtask = {},
)
}
}
@Preview(showBackground = true, widthDp = 320)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 320)
@Composable
fun SubtasksPreview() {
MdcTheme {
SubtaskRow(
filter = null,
googleTask = null,
desaturate = true,
existingSubtasks = listOf(
TaskContainer().apply {
task = Task().apply {
title = "Existing subtask 1"
priority = Task.Priority.HIGH
}
indent = 0
},
TaskContainer().apply {
task = Task().apply {
title = "Existing subtask 2"
priority = Task.Priority.LOW
}
indent = 1
}
),
newSubtasks = listOf(
Task().apply {
title = "New subtask 1"
},
Task().apply {
title = "New subtask 2"
},
Task(),
),
openSubtask = {},
completeExistingSubtask = { _, _ -> },
completeNewSubtask = {},
toggleSubtask = { _, _ -> },
addSubtask = {},
deleteSubtask = {},
)
}
}

@ -39,7 +39,7 @@ class LocationControlSet : TaskEditControlFragment() {
viewModel.selectedLocation.value = location
}
override fun onRowClick() {
private fun onRowClick() {
val location = viewModel.selectedLocation.value
if (location == null) {
chooseLocation()

@ -5,37 +5,18 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.ContentAlpha
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextDecoration.Companion.LineThrough
import androidx.compose.ui.text.style.TextDecoration.Companion.None
import androidx.compose.ui.unit.dp
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import com.google.android.material.composethemeadapter.MdcTheme
import com.todoroo.andlib.sql.Criterion
import com.todoroo.andlib.sql.Join
import com.todoroo.andlib.sql.QueryTemplate
import com.todoroo.andlib.utility.DateUtilities.now
import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.api.GtasksFilter
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.service.TaskCompleter
@ -44,10 +25,10 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.compose.*
import org.tasks.compose.collectAsStateLifecycleAware
import org.tasks.compose.edit.SubtaskRow
import org.tasks.data.GoogleTask
import org.tasks.data.GoogleTaskDao
import org.tasks.data.TaskContainer
import org.tasks.data.TaskDao.TaskCriteria.activeAndVisible
import org.tasks.preferences.Preferences
import org.tasks.themes.ColorProvider
@ -76,50 +57,40 @@ class SubtaskControlSet : TaskEditControlFragment() {
}
}
@Composable
override fun Body() {
Column {
val filter = viewModel.selectedList.collectAsStateLifecycleAware().value
val googleTask = googleTaskDao.watchGoogleTask(viewModel.task.id)
.collectAsStateLifecycleAware(initial = null).value
val isGoogleTaskChild =
filter is GtasksFilter && googleTask != null && googleTask.parent > 0 && googleTask.listId == filter.remoteId
if (isGoogleTaskChild) {
DisabledText(
text = stringResource(id = R.string.subtasks_multilevel_google_task),
modifier = Modifier.padding(vertical = 20.dp)
)
} else {
val subtasks = listViewModel.tasks.observeAsState(initial = emptyList()).value
val newSubtasks = viewModel.newSubtasks.collectAsStateLifecycleAware().value
Spacer(modifier = Modifier.height(height = 8.dp))
ExistingSubtasks(subtasks = subtasks, multiLevelSubtasks = filter !is GtasksFilter)
NewSubtasks(
subtasks = newSubtasks,
onComplete = {
val copy = ArrayList(viewModel.newSubtasks.value)
copy[copy.indexOf(it)] =
it.clone().apply { completionDate = if (isCompleted) 0 else now() }
viewModel.newSubtasks.value = copy
},
onDelete = {
val copy = ArrayList(viewModel.newSubtasks.value)
copy.remove(it)
viewModel.newSubtasks.value = copy
}
)
DisabledText(
text = stringResource(id = R.string.TEA_add_subtask),
modifier = Modifier
.clickable { addSubtask() }
.padding(12.dp)
)
Spacer(modifier = Modifier.height(8.dp))
override fun bind(parent: ViewGroup?): View =
(parent as ComposeView).apply {
setContent {
MdcTheme {
SubtaskRow(
filter = viewModel.selectedList.collectAsStateLifecycleAware().value,
googleTask = googleTaskDao.watchGoogleTask(viewModel.task.id).collectAsStateLifecycleAware(initial = null).value,
desaturate = preferences.desaturateDarkMode,
existingSubtasks = listViewModel.tasks.observeAsState(initial = emptyList()).value,
newSubtasks = viewModel.newSubtasks.collectAsStateLifecycleAware().value,
openSubtask = this@SubtaskControlSet::openSubtask,
completeExistingSubtask = this@SubtaskControlSet::complete,
toggleSubtask = this@SubtaskControlSet::toggleSubtask,
addSubtask = this@SubtaskControlSet::addSubtask,
completeNewSubtask = {
viewModel.newSubtasks.value =
ArrayList(viewModel.newSubtasks.value).apply {
val modified = it.clone().apply {
completionDate =
if (isCompleted) 0 else now()
}
set(indexOf(it), modified)
}
},
deleteSubtask = {
viewModel.newSubtasks.value =
ArrayList(viewModel.newSubtasks.value).apply {
remove(it)
}
}
)
}
}
}
}
override val icon = R.drawable.ic_subdirectory_arrow_right_black_24dp
override fun controlId() = TAG
@ -157,134 +128,6 @@ class SubtaskControlSet : TaskEditControlFragment() {
}
}
@Composable
override fun Icon() {
TaskEditIcon(
id = icon,
modifier = Modifier
.padding(start = 16.dp, top = 20.dp, end = 20.dp, bottom = 20.dp)
.alpha(ContentAlpha.medium),
)
}
@Composable
fun NewSubtasks(
subtasks: List<Task>,
onComplete: (Task) -> Unit,
onDelete: (Task) -> Unit,
) {
subtasks.forEach { subtask ->
NewSubtaskRow(
subtask = subtask,
onComplete = onComplete,
onDelete = onDelete,
)
}
}
@Composable
fun ExistingSubtasks(subtasks: List<TaskContainer>, multiLevelSubtasks: Boolean) {
subtasks.forEach { task ->
SubtaskRow(
task = task,
indent = if (multiLevelSubtasks) task.indent else 0,
onRowClick = { openSubtask(task.task) },
onCompleteClick = { complete(task.task, !task.isCompleted) },
onToggleSubtaskClick = { toggleSubtask(task.id, !task.isCollapsed) }
)
}
}
@Composable
fun NewSubtaskRow(
subtask: Task,
onComplete: (Task) -> Unit,
onDelete: (Task) -> Unit,
) {
Row(verticalAlignment = Alignment.CenterVertically) {
CheckBox(
task = subtask,
onCompleteClick = { onComplete(subtask) },
modifier = Modifier.align(Alignment.Top),
desaturate = preferences.desaturateDarkMode,
)
var text by remember { mutableStateOf(subtask.title ?: "") }
val focusRequester = remember { FocusRequester() }
BasicTextField(
value = text,
onValueChange = {
text = it
subtask.title = it
},
cursorBrush = SolidColor(MaterialTheme.colors.onSurface),
modifier = Modifier
.weight(1f)
.focusable(enabled = true)
.focusRequester(focusRequester)
.alpha(if (subtask.isCompleted) ContentAlpha.disabled else ContentAlpha.high),
textStyle = MaterialTheme.typography.body1.copy(
textDecoration = if (subtask.isCompleted) LineThrough else None,
color = MaterialTheme.colors.onSurface,
),
keyboardOptions = KeyboardOptions.Default.copy(
capitalization = KeyboardCapitalization.Sentences,
imeAction = ImeAction.Done,
),
keyboardActions = KeyboardActions(
onDone = {
if (text.isNotBlank()) {
addSubtask()
}
}
),
singleLine = true,
maxLines = Int.MAX_VALUE,
)
ClearButton { onDelete(subtask) }
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
}
@Composable
fun SubtaskRow(
task: TaskContainer, indent: Int,
onRowClick: () -> Unit,
onCompleteClick: () -> Unit,
onToggleSubtaskClick: () -> Unit,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable { onRowClick() }
.padding(end = 16.dp)
) {
Spacer(modifier = Modifier.width((indent * 20).dp))
CheckBox(
task = task.task,
onCompleteClick = onCompleteClick,
desaturate = preferences.desaturateDarkMode
)
Text(
text = task.title,
modifier = Modifier
.weight(1f)
.alpha(if (task.isCompleted || task.isHidden) ContentAlpha.disabled else ContentAlpha.high),
style = MaterialTheme.typography.body1.copy(
textDecoration = if (task.isCompleted) LineThrough else None
)
)
if (task.hasChildren()) {
SubtaskChip(
task = task,
compact = true,
onClick = onToggleSubtaskClick,
)
}
}
}
companion object {
const val TAG = R.string.TEA_ctrl_subtask_pref
private fun getQueryTemplate(task: Task): QueryTemplate = QueryTemplate()
@ -306,4 +149,3 @@ class SubtaskControlSet : TaskEditControlFragment() {
)
}
}

@ -4,32 +4,13 @@ import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ContentAlpha
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.unit.dp
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.google.android.material.composethemeadapter.MdcTheme
import org.tasks.compose.TaskEditIcon
import org.tasks.compose.TaskEditRow
abstract class TaskEditControlFragment : Fragment() {
lateinit var viewModel: TaskEditViewModel
protected open fun createView(savedInstanceState: Bundle?) {}
protected open fun onRowClick() {}
protected open val isClickable: Boolean
get() = false
protected open val icon = 0
abstract fun controlId(): Int
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -42,32 +23,9 @@ abstract class TaskEditControlFragment : Fragment() {
return composeView
}
open fun bind(parent: ViewGroup?): View =
(parent as ComposeView).apply {
setContent {
MdcTheme {
TaskEditRow(
icon = { Icon() },
content = { Body() },
onClick = if (this@TaskEditControlFragment.isClickable)
this@TaskEditControlFragment::onRowClick
else
null
)
}
}
}
abstract fun bind(parent: ViewGroup?): View
@Composable
protected open fun Icon() {
TaskEditIcon(
id = icon,
modifier = Modifier
.padding(start = 16.dp, top = 20.dp, end = 32.dp, bottom = 20.dp)
.alpha(ContentAlpha.medium),
)
}
protected open fun createView(savedInstanceState: Bundle?) {}
@Composable
protected open fun Body() {}
abstract fun controlId(): Int
}
Loading…
Cancel
Save