Add SubtaskRow composable

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

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

@ -55,7 +55,7 @@ class TimerControlSet : TaskEditControlFragment() {
callback = activity as TimerControlSetCallback callback = activity as TimerControlSetCallback
} }
override fun onRowClick() { private fun onRowClick() {
if (dialog == null) { if (dialog == null) {
dialog = buildDialog() 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 viewModel.selectedLocation.value = location
} }
override fun onRowClick() { private fun onRowClick() {
val location = viewModel.selectedLocation.value val location = viewModel.selectedLocation.value
if (location == null) { if (location == null) {
chooseLocation() chooseLocation()

@ -5,37 +5,18 @@ import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.compose.foundation.clickable import android.view.View
import androidx.compose.foundation.focusable import android.view.ViewGroup
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.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment import androidx.compose.ui.platform.ComposeView
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.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.composethemeadapter.MdcTheme
import com.todoroo.andlib.sql.Criterion import com.todoroo.andlib.sql.Criterion
import com.todoroo.andlib.sql.Join import com.todoroo.andlib.sql.Join
import com.todoroo.andlib.sql.QueryTemplate import com.todoroo.andlib.sql.QueryTemplate
import com.todoroo.andlib.utility.DateUtilities.now import com.todoroo.andlib.utility.DateUtilities.now
import com.todoroo.astrid.api.Filter import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.api.GtasksFilter
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task import com.todoroo.astrid.data.Task
import com.todoroo.astrid.service.TaskCompleter import com.todoroo.astrid.service.TaskCompleter
@ -44,10 +25,10 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.tasks.LocalBroadcastManager import org.tasks.LocalBroadcastManager
import org.tasks.R 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.GoogleTask
import org.tasks.data.GoogleTaskDao import org.tasks.data.GoogleTaskDao
import org.tasks.data.TaskContainer
import org.tasks.data.TaskDao.TaskCriteria.activeAndVisible import org.tasks.data.TaskDao.TaskCriteria.activeAndVisible
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.themes.ColorProvider import org.tasks.themes.ColorProvider
@ -76,51 +57,41 @@ class SubtaskControlSet : TaskEditControlFragment() {
} }
} }
@Composable override fun bind(parent: ViewGroup?): View =
override fun Body() { (parent as ComposeView).apply {
Column { setContent {
val filter = viewModel.selectedList.collectAsStateLifecycleAware().value MdcTheme {
val googleTask = googleTaskDao.watchGoogleTask(viewModel.task.id) SubtaskRow(
.collectAsStateLifecycleAware(initial = null).value filter = viewModel.selectedList.collectAsStateLifecycleAware().value,
val isGoogleTaskChild = googleTask = googleTaskDao.watchGoogleTask(viewModel.task.id).collectAsStateLifecycleAware(initial = null).value,
filter is GtasksFilter && googleTask != null && googleTask.parent > 0 && googleTask.listId == filter.remoteId desaturate = preferences.desaturateDarkMode,
if (isGoogleTaskChild) { existingSubtasks = listViewModel.tasks.observeAsState(initial = emptyList()).value,
DisabledText( newSubtasks = viewModel.newSubtasks.collectAsStateLifecycleAware().value,
text = stringResource(id = R.string.subtasks_multilevel_google_task), openSubtask = this@SubtaskControlSet::openSubtask,
modifier = Modifier.padding(vertical = 20.dp) completeExistingSubtask = this@SubtaskControlSet::complete,
) toggleSubtask = this@SubtaskControlSet::toggleSubtask,
} else { addSubtask = this@SubtaskControlSet::addSubtask,
val subtasks = listViewModel.tasks.observeAsState(initial = emptyList()).value completeNewSubtask = {
val newSubtasks = viewModel.newSubtasks.collectAsStateLifecycleAware().value viewModel.newSubtasks.value =
Spacer(modifier = Modifier.height(height = 8.dp)) ArrayList(viewModel.newSubtasks.value).apply {
ExistingSubtasks(subtasks = subtasks, multiLevelSubtasks = filter !is GtasksFilter) val modified = it.clone().apply {
NewSubtasks( completionDate =
subtasks = newSubtasks, if (isCompleted) 0 else now()
onComplete = { }
val copy = ArrayList(viewModel.newSubtasks.value) set(indexOf(it), modified)
copy[copy.indexOf(it)] = }
it.clone().apply { completionDate = if (isCompleted) 0 else now() }
viewModel.newSubtasks.value = copy
}, },
onDelete = { deleteSubtask = {
val copy = ArrayList(viewModel.newSubtasks.value) viewModel.newSubtasks.value =
copy.remove(it) ArrayList(viewModel.newSubtasks.value).apply {
viewModel.newSubtasks.value = copy remove(it)
}
} }
) )
DisabledText(
text = stringResource(id = R.string.TEA_add_subtask),
modifier = Modifier
.clickable { addSubtask() }
.padding(12.dp)
)
Spacer(modifier = Modifier.height(8.dp))
} }
} }
} }
override val icon = R.drawable.ic_subdirectory_arrow_right_black_24dp
override fun controlId() = TAG override fun controlId() = TAG
override fun onResume() { override fun onResume() {
@ -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 { companion object {
const val TAG = R.string.TEA_ctrl_subtask_pref const val TAG = R.string.TEA_ctrl_subtask_pref
private fun getQueryTemplate(task: Task): QueryTemplate = QueryTemplate() 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.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup 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.platform.ComposeView
import androidx.compose.ui.unit.dp
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider 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() { abstract class TaskEditControlFragment : Fragment() {
lateinit var viewModel: TaskEditViewModel 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( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
@ -42,32 +23,9 @@ abstract class TaskEditControlFragment : Fragment() {
return composeView return composeView
} }
open fun bind(parent: ViewGroup?): View = abstract 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
)
}
}
}
@Composable protected open fun createView(savedInstanceState: Bundle?) {}
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),
)
}
@Composable abstract fun controlId(): Int
protected open fun Body() {}
} }
Loading…
Cancel
Save