Convert subtask control set to compose

pull/1934/head
Alex Baker 2 years ago
parent 8c137f6521
commit b4c3bec3ab

@ -5,11 +5,7 @@ import android.os.Parcel
import android.os.Parcelable
import androidx.annotation.IntDef
import androidx.core.os.ParcelCompat
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.Index
import androidx.room.PrimaryKey
import androidx.room.*
import com.google.gson.annotations.SerializedName
import com.todoroo.andlib.data.Table
import com.todoroo.andlib.sql.Field
@ -473,6 +469,15 @@ class Task : Parcelable {
}
}
fun clone(): Task {
val parcel = Parcel.obtain()
writeToParcel(parcel, 0)
parcel.setDataPosition(0)
val task = Task(parcel)
parcel.recycle()
return task
}
companion object {
const val TABLE_NAME = "tasks"
// --- table and uri

@ -1,29 +1,18 @@
package org.tasks.compose
import android.content.res.Configuration
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.darkColors
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Clear
import androidx.compose.material.lightColors
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.content.res.ResourcesCompat
import org.tasks.R
@Composable
@ -42,24 +31,6 @@ fun AlarmRow(text: String, remove: () -> Unit = {}) {
}
}
@Composable
fun ClearButton(onClick: () -> Unit) {
Icon(
imageVector = Icons.Outlined.Clear,
modifier = Modifier
.padding(12.dp)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = onClick
)
.alpha(
ResourcesCompat.getFloat(LocalContext.current.resources, R.dimen.alpha_secondary)
),
contentDescription = stringResource(id = R.string.delete)
)
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable

@ -0,0 +1,23 @@
package org.tasks.compose
import androidx.compose.material.ContentAlpha
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Clear
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.stringResource
import org.tasks.R
@Composable
fun ClearButton(onClick: () -> Unit) {
IconButton(onClick = onClick) {
Icon(
imageVector = Icons.Outlined.Clear,
modifier = Modifier.alpha(ContentAlpha.medium),
contentDescription = stringResource(id = R.string.delete)
)
}
}

@ -1,13 +1,8 @@
package org.tasks.data
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.RoomWarnings
import androidx.room.Transaction
import androidx.room.Update
import androidx.room.*
import com.todoroo.astrid.data.Task
import kotlinx.coroutines.flow.Flow
import org.tasks.db.SuspendDbUtils.chunkedMap
import org.tasks.time.DateTimeUtils.currentTimeMillis
@ -67,6 +62,9 @@ abstract class GoogleTaskDao {
@Query("SELECT * FROM google_tasks WHERE gt_task = :taskId AND gt_deleted = 0 LIMIT 1")
abstract suspend fun getByTaskId(taskId: Long): GoogleTask?
@Query("SELECT * FROM google_tasks WHERE gt_task = :taskId AND gt_deleted = 0 LIMIT 1")
abstract fun watchGoogleTask(taskId: Long): Flow<GoogleTask?>
@Update
abstract suspend fun update(googleTask: GoogleTask)

@ -1,112 +0,0 @@
package org.tasks.tasklist
import android.graphics.Paint
import android.util.DisplayMetrics
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.MarginLayoutParams
import android.widget.TextView
import androidx.compose.ui.platform.ComposeView
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.composethemeadapter.MdcTheme
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.ui.CheckableImageView
import org.tasks.data.TaskContainer
import org.tasks.databinding.SubtaskAdapterRowBodyBinding
import org.tasks.ui.CheckBoxProvider
import org.tasks.ui.ChipProvider
import kotlin.math.roundToInt
class SubtaskViewHolder internal constructor(
binding: SubtaskAdapterRowBodyBinding,
private val callbacks: Callbacks,
private val metrics: DisplayMetrics,
private val chipProvider: ChipProvider,
private val checkBoxProvider: CheckBoxProvider
) : RecyclerView.ViewHolder(binding.root) {
private var task: TaskContainer? = null
private val rowBody: ViewGroup
private val nameView: TextView
private val completeBox: CheckableImageView
private val chipGroup: ComposeView
init {
rowBody = binding.rowBody
nameView = binding.title
completeBox = binding.completeBox
chipGroup = binding.chipGroup
nameView.setOnClickListener { v: View? -> openSubtask() }
completeBox.setOnClickListener { v: View? -> onCompleteBoxClick() }
val view: ViewGroup = binding.root
view.tag = this
for (i in 0 until view.childCount) {
view.getChildAt(i).tag = this
}
}
private val shiftSize: Float
get() = 20 * metrics.density
private fun getIndentSize(indent: Int): Int {
return (indent * shiftSize).roundToInt()
}
fun bindView(task: TaskContainer) {
this.task = task
setIndent(task.indent)
chipGroup.setContent {
MdcTheme {
if (task.hasChildren()) {
chipProvider.SubtaskChip(task = task, compact = true) {
callbacks.toggleSubtask(task.id, !task.isCollapsed)
}
}
}
}
nameView.text = task.title
setupTitleAndCheckbox()
}
private fun setupTitleAndCheckbox() {
if (task!!.isCompleted) {
nameView.isEnabled = false
nameView.paintFlags = nameView.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
} else {
nameView.isEnabled = !task!!.isHidden
nameView.paintFlags = nameView.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv()
}
completeBox.isChecked = task!!.isCompleted
completeBox.setImageDrawable(checkBoxProvider.getCheckBox(task!!.getTask()))
completeBox.invalidate()
}
private fun openSubtask() {
callbacks.openSubtask(task!!.getTask())
}
private fun onCompleteBoxClick() {
if (task == null) {
return
}
val newState = completeBox.isChecked
if (newState != task!!.isCompleted) {
callbacks.complete(task!!.getTask(), newState)
}
// set check box to actual action item state
setupTitleAndCheckbox()
}
private fun setIndent(indent: Int) {
val indentSize = getIndentSize(indent)
val layoutParams = rowBody.layoutParams as MarginLayoutParams
layoutParams.marginStart = indentSize
rowBody.layoutParams = layoutParams
}
interface Callbacks {
fun openSubtask(task: Task)
fun toggleSubtask(taskId: Long, collapsed: Boolean)
fun complete(task: Task, completed: Boolean)
}
}

@ -1,105 +0,0 @@
package org.tasks.tasklist;
import android.app.Activity;
import android.util.DisplayMetrics;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.AsyncDifferConfig;
import androidx.recyclerview.widget.AsyncListDiffer;
import androidx.recyclerview.widget.ListUpdateCallback;
import androidx.recyclerview.widget.RecyclerView;
import org.tasks.data.TaskContainer;
import org.tasks.databinding.SubtaskAdapterRowBodyBinding;
import org.tasks.tasklist.SubtaskViewHolder.Callbacks;
import org.tasks.ui.CheckBoxProvider;
import org.tasks.ui.ChipProvider;
import java.util.List;
public class SubtasksRecyclerAdapter extends RecyclerView.Adapter<SubtaskViewHolder>
implements ListUpdateCallback {
private final DisplayMetrics metrics;
private final Activity activity;
private final ChipProvider chipProvider;
private final CheckBoxProvider checkBoxProvider;
private final Callbacks callbacks;
private final AsyncListDiffer<TaskContainer> differ;
private boolean multiLevelSubtasks;
public SubtasksRecyclerAdapter(
Activity activity,
ChipProvider chipProvider,
CheckBoxProvider checkBoxProvider,
SubtaskViewHolder.Callbacks callbacks) {
this.activity = activity;
this.chipProvider = chipProvider;
this.checkBoxProvider = checkBoxProvider;
this.callbacks = callbacks;
differ =
new AsyncListDiffer<>(
this, new AsyncDifferConfig.Builder<>(new ItemCallback()).build());
metrics = activity.getResources().getDisplayMetrics();
}
@NonNull
@Override
public SubtaskViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new SubtaskViewHolder(
SubtaskAdapterRowBodyBinding.inflate(LayoutInflater.from(activity), parent, false),
callbacks,
metrics,
chipProvider,
checkBoxProvider
);
}
@Override
public void onBindViewHolder(@NonNull SubtaskViewHolder holder, int position) {
TaskContainer task = differ.getCurrentList().get(position);
if (task != null) {
task.setIndent(multiLevelSubtasks ? task.indent : 0);
holder.bindView(task);
}
}
public void submitList(List<TaskContainer> list) {
differ.submitList(list);
}
@Override
public void onInserted(int position, int count) {
notifyItemRangeInserted(position, count);
}
@Override
public void onRemoved(int position, int count) {
notifyDataSetChanged(); // remove animation is janky
}
@Override
public void onMoved(int fromPosition, int toPosition) {
notifyItemMoved(fromPosition, toPosition);
}
@Override
public void onChanged(int position, int count, @Nullable Object payload) {
notifyItemRangeChanged(position, count, payload);
}
@Override
public int getItemCount() {
return differ.getCurrentList().size();
}
public void setMultiLevelSubtasksEnabled(boolean enabled) {
if (multiLevelSubtasks != enabled) {
multiLevelSubtasks = enabled;
notifyItemRangeChanged(0, differ.getCurrentList().size());
}
}
}

@ -12,25 +12,19 @@ import org.tasks.themes.ColorProvider
import org.tasks.themes.DrawableUtil
import javax.inject.Inject
class CheckBoxProvider @Inject constructor(@param:ActivityContext private val context: Context, private val colorProvider: ColorProvider) {
fun getCheckBox(task: Task) = getCheckBox(task.isCompleted, task.isRecurring, task.priority)
fun getCheckBox(complete: Boolean, repeating: Boolean, priority: Int) =
getDrawable(getDrawableRes(complete, repeating), priority)
class CheckBoxProvider @Inject constructor(
@param:ActivityContext private val context: Context,
private val colorProvider: ColorProvider
) {
fun getCheckBox(task: Task) = getDrawable(task.getCheckboxRes(), task.priority)
fun getWidgetCheckBox(task: Task): Bitmap {
val wrapped = DrawableUtil.getWrapped(context, getDrawableRes(task.isCompleted, task.isRecurring))
val wrapped =
DrawableUtil.getWrapped(context, task.getCheckboxRes())
DrawableUtil.setTint(wrapped, colorProvider.getPriorityColor(task.priority, false))
return convertToBitmap(wrapped)
}
private fun getDrawableRes(complete: Boolean, repeating: Boolean) = when {
complete -> R.drawable.ic_outline_check_box_24px
repeating -> R.drawable.ic_outline_repeat_24px
else -> R.drawable.ic_outline_check_box_outline_blank_24px
}
private fun getDrawable(@DrawableRes resId: Int, priority: Int): Drawable {
val original = context.getDrawable(resId)
val wrapped = original!!.mutate()
@ -39,10 +33,19 @@ class CheckBoxProvider @Inject constructor(@param:ActivityContext private val co
}
private fun convertToBitmap(d: Drawable): Bitmap {
val bitmap = Bitmap.createBitmap(d.intrinsicWidth, d.intrinsicHeight, Bitmap.Config.ARGB_8888)
val bitmap =
Bitmap.createBitmap(d.intrinsicWidth, d.intrinsicHeight, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
d.setBounds(0, 0, canvas.width, canvas.height)
d.draw(canvas)
return bitmap
}
companion object {
fun Task.getCheckboxRes() = when {
isCompleted -> R.drawable.ic_outline_check_box_24px
isRecurring -> R.drawable.ic_outline_repeat_24px
else -> R.drawable.ic_outline_check_box_outline_blank_24px
}
}
}

@ -4,24 +4,31 @@ import android.app.Activity
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.graphics.Paint
import android.os.Bundle
import android.text.Editable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.LinearLayout
import androidx.core.widget.addTextChangedListener
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.*
import androidx.compose.runtime.*
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.Color
import androidx.compose.ui.res.painterResource
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.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.todoroo.andlib.sql.Criterion
import com.todoroo.andlib.sql.Join
import com.todoroo.andlib.sql.QueryTemplate
@ -32,79 +39,87 @@ import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.service.TaskCompleter
import com.todoroo.astrid.service.TaskCreator
import com.todoroo.astrid.ui.CheckableImageView
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.data.CaldavDao
import org.tasks.compose.ClearButton
import org.tasks.compose.DisabledText
import org.tasks.compose.TaskEditIcon
import org.tasks.compose.collectAsStateLifecycleAware
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.databinding.ControlSetSubtasksBinding
import org.tasks.extensions.Context.toast
import org.tasks.tasklist.SubtaskViewHolder
import org.tasks.tasklist.SubtasksRecyclerAdapter
import org.tasks.themes.ColorProvider
import org.tasks.ui.CheckBoxProvider.Companion.getCheckboxRes
import javax.inject.Inject
@AndroidEntryPoint
class SubtaskControlSet : TaskEditControlFragment(), SubtaskViewHolder.Callbacks {
private lateinit var recyclerView: RecyclerView
private lateinit var newSubtaskContainer: LinearLayout
class SubtaskControlSet : TaskEditControlComposeFragment() {
@Inject lateinit var activity: Activity
@Inject lateinit var taskCompleter: TaskCompleter
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var googleTaskDao: GoogleTaskDao
@Inject lateinit var taskCreator: TaskCreator
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var checkBoxProvider: CheckBoxProvider
@Inject lateinit var chipProvider: ChipProvider
@Inject lateinit var eventBus: MainActivityEventBus
@Inject lateinit var colorProvider: ColorProvider
private val listViewModel: TaskListViewModel by viewModels()
private val refreshReceiver = RefreshReceiver()
private var remoteList: Filter? = null
private var googleTask: GoogleTask? = null
private lateinit var recyclerAdapter: SubtasksRecyclerAdapter
override fun createView(savedInstanceState: Bundle?) {
viewModel.newSubtasks.forEach { addSubtask(it) }
recyclerAdapter = SubtasksRecyclerAdapter(activity, chipProvider, checkBoxProvider, this)
viewModel.task.let {
if (it.id > 0) {
recyclerAdapter.submitList(listViewModel.value)
listViewModel.setFilter(Filter("subtasks", getQueryTemplate(it)))
(recyclerView.itemAnimator as DefaultItemAnimator).supportsChangeAnimations = false
recyclerView.layoutManager = LinearLayoutManager(activity)
recyclerView.isNestedScrollingEnabled = false
listViewModel.observe(this) {
list: List<TaskContainer?>? -> recyclerAdapter.submitList(list)
}
recyclerView.adapter = recyclerAdapter
}
viewModel.task.takeIf { it.id > 0 }?.let {
listViewModel.setFilter(Filter("subtasks", getQueryTemplate(it)))
}
}
lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
viewModel.selectedList.collect {
remoteList = it
updateUI()
}
@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?) =
ControlSetSubtasksBinding.inflate(layoutInflater, parent, true).let {
recyclerView = it.recyclerView
newSubtaskContainer = it.newSubtasks
it.addSubtask.setOnClickListener { addSubtask() }
it.root
}
override val icon = R.drawable.ic_subdirectory_arrow_right_black_24dp
override fun controlId() = TAG
@ -112,12 +127,6 @@ class SubtaskControlSet : TaskEditControlFragment(), SubtaskViewHolder.Callbacks
override fun onResume() {
super.onResume()
localBroadcastManager.registerRefreshReceiver(refreshReceiver)
lifecycleScope.launch {
viewModel.task.let {
googleTask = googleTaskDao.getByTaskId(it.id)
updateUI()
}
}
}
override fun onPause() {
@ -125,122 +134,187 @@ class SubtaskControlSet : TaskEditControlFragment(), SubtaskViewHolder.Callbacks
localBroadcastManager.unregisterReceiver(refreshReceiver)
}
private fun addSubtask() {
if (isGoogleTaskChild) {
context?.toast(R.string.subtasks_multilevel_google_task)
} else {
lifecycleScope.launch {
val task = taskCreator.createWithValues("")
viewModel.newSubtasks.add(task)
val editText = addSubtask(task)
editText.requestFocus()
val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(editText, InputMethodManager.SHOW_IMPLICIT)
}
}
private fun addSubtask() = lifecycleScope.launch {
val task = taskCreator.createWithValues("")
viewModel.newSubtasks.value = viewModel.newSubtasks.value.plus(task)
}
private fun addSubtask(task: Task): EditText {
val view = LayoutInflater.from(activity)
.inflate(R.layout.editable_subtask_adapter_row_body, newSubtaskContainer, false) as ViewGroup
view.findViewById<View>(R.id.clear).setOnClickListener { newSubtaskContainer.removeView(view) }
val editText = view.getChildAt(2) as EditText
editText.setText(task.title)
editText.setHorizontallyScrolling(false)
editText.setLines(1)
editText.maxLines = Int.MAX_VALUE
editText.isFocusable = true
editText.isEnabled = true
editText.addTextChangedListener { text: Editable? ->
task.title = text?.toString()
}
editText.setOnEditorActionListener { _, actionId: Int, _ ->
if (actionId == EditorInfo.IME_ACTION_NEXT) {
if (editText.text.isNotEmpty()) {
addSubtask()
}
return@setOnEditorActionListener true
}
false
}
val completeBox: CheckableImageView = view.findViewById(R.id.completeBox)
completeBox.isChecked = task.isCompleted
updateCompleteBox(task, completeBox, editText)
completeBox.setOnClickListener { updateCompleteBox(task, completeBox, editText) }
newSubtaskContainer.addView(view)
return editText
}
private fun updateCompleteBox(task: Task, completeBox: CheckableImageView, editText: EditText) {
val isComplete = completeBox.isChecked
task.completionDate = if (isComplete) now() else 0
completeBox.setImageDrawable(
checkBoxProvider.getCheckBox(isComplete, false, task.priority))
editText.paintFlags = if (isComplete) {
editText.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
} else {
editText.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv()
}
private fun openSubtask(task: Task) = lifecycleScope.launch {
eventBus.emit(MainActivityEvent.OpenTask(task))
}
private val isGoogleTaskChild: Boolean
get() = (remoteList is GtasksFilter
&& googleTask != null && googleTask!!.parent > 0 && googleTask!!.listId == (remoteList as GtasksFilter).remoteId)
private fun toggleSubtask(taskId: Long, collapsed: Boolean) = lifecycleScope.launch {
taskDao.setCollapsed(taskId, collapsed)
localBroadcastManager.broadcastRefresh()
}
private fun complete(task: Task, completed: Boolean) = lifecycleScope.launch {
taskCompleter.setComplete(task, completed)
}
private fun updateUI() {
if (isGoogleTaskChild) {
recyclerView.visibility = View.GONE
newSubtaskContainer.visibility = View.GONE
} else {
recyclerView.visibility = View.VISIBLE
newSubtaskContainer.visibility = View.VISIBLE
recyclerAdapter.setMultiLevelSubtasksEnabled(remoteList !is GtasksFilter)
refresh()
private inner class RefreshReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
listViewModel.invalidate()
}
}
private fun refresh() {
listViewModel.invalidate()
@Composable
override fun Icon() {
TaskEditIcon(
id = icon,
modifier = Modifier.padding(start = 16.dp, top = 20.dp, end = 20.dp, bottom = 20.dp),
)
}
override fun openSubtask(task: Task) {
lifecycleScope.launch {
eventBus.emit(MainActivityEvent.OpenTask(task))
@Composable
fun NewSubtasks(
subtasks: List<Task>,
onComplete: (Task) -> Unit,
onDelete: (Task) -> Unit,
) {
subtasks.forEach { subtask ->
NewSubtaskRow(
subtask = subtask,
onComplete = onComplete,
onDelete = onDelete,
)
}
}
override fun toggleSubtask(taskId: Long, collapsed: Boolean) {
lifecycleScope.launch {
taskDao.setCollapsed(taskId, collapsed)
localBroadcastManager.broadcastRefresh()
@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) }
)
}
}
override fun complete(task: Task, completed: Boolean) {
lifecycleScope.launch {
taskCompleter.setComplete(task, completed)
@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)
)
var text by remember { mutableStateOf(subtask.title ?: "") }
val focusRequester = remember { FocusRequester() }
BasicTextField(
value = text,
onValueChange = {
text = it
subtask.title = it
},
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
),
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()
}
}
}
private inner class RefreshReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
refresh()
@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)
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()) {
chipProvider.SubtaskChip(
task = task,
compact = true,
onClick = onToggleSubtaskClick,
)
}
}
}
@Composable
fun CheckBox(
task: Task,
onCompleteClick: () -> Unit,
modifier: Modifier = Modifier,
) {
IconButton(onClick = onCompleteClick, modifier = modifier) {
Icon(
painter = painterResource(id = task.getCheckboxRes()),
tint = Color(
colorProvider.getPriorityColor(
priority = task.priority,
adjust = false
)
),
contentDescription = null
)
}
}
companion object {
const val TAG = R.string.TEA_ctrl_subtask_pref
private fun getQueryTemplate(task: Task): QueryTemplate = QueryTemplate()
.join(
Join.left(
GoogleTask.TABLE,
Criterion.and(
GoogleTask.PARENT.eq(task.id),
GoogleTask.TASK.eq(Task.ID),
GoogleTask.DELETED.eq(0))))
.where(
Criterion.and(
activeAndVisible(),
Criterion.or(Task.PARENT.eq(task.id), GoogleTask.TASK.gt(0))))
}
}
.join(
Join.left(
GoogleTask.TABLE,
Criterion.and(
GoogleTask.PARENT.eq(task.id),
GoogleTask.TASK.eq(Task.ID),
GoogleTask.DELETED.eq(0)
)
)
)
.where(
Criterion.and(
activeAndVisible(),
Criterion.or(Task.PARENT.eq(task.id), GoogleTask.TASK.gt(0))
)
)
}
}

@ -254,7 +254,7 @@ class TaskEditViewModel @Inject constructor(
val selectedTags = MutableStateFlow(ArrayList<TagData>())
var newSubtasks = ArrayList<Task>()
var newSubtasks = MutableStateFlow(emptyList<Task>())
private lateinit var originalAlarms: List<Alarm>
@ -307,7 +307,7 @@ class TaskEditViewModel @Inject constructor(
originalList != selectedList.value ||
originalLocation != selectedLocation.value ||
originalTags.toHashSet() != selectedTags.value.toHashSet() ||
newSubtasks.isNotEmpty() ||
newSubtasks.value.isNotEmpty() ||
getRingFlags() != when {
task.isNotifyModeFive -> NOTIFY_MODE_FIVE
task.isNotifyModeNonstop -> NOTIFY_MODE_NONSTOP
@ -373,7 +373,7 @@ class TaskEditViewModel @Inject constructor(
task.modificationDate = currentTimeMillis()
}
for (subtask in newSubtasks) {
for (subtask in newSubtasks.value) {
if (Strings.isNullOrEmpty(subtask.title)) {
continue
}

@ -29,7 +29,9 @@ class TaskListViewModel @Inject constructor(
private val preferences: Preferences,
private val taskDao: TaskDao) : ViewModel(), Observer<PagedList<TaskContainer>> {
private var tasks = MutableLiveData<List<TaskContainer>>()
private var _tasks = MutableLiveData<List<TaskContainer>>()
val tasks: LiveData<List<TaskContainer>>
get() = _tasks
private var filter: Filter? = null
private var manualSortFilter = false
private var internal: LiveData<PagedList<TaskContainer>>? = null
@ -39,13 +41,13 @@ class TaskListViewModel @Inject constructor(
|| filter.supportsAstridSorting() && preferences.isAstridSort)
if (filter != this.filter || filter.getSqlQuery() != this.filter!!.getSqlQuery()) {
this.filter = filter
tasks = MutableLiveData()
_tasks = MutableLiveData()
invalidate()
}
}
fun observe(owner: LifecycleOwner, observer: (List<TaskContainer>) -> Unit) =
tasks.observe(owner, observer)
_tasks.observe(owner, observer)
fun searchByFilter(filter: Filter?) {
this.filter = filter
@ -75,7 +77,7 @@ class TaskListViewModel @Inject constructor(
}
private suspend fun performNonPagedQuery(subtasks: SubtaskInfo) {
tasks.value = taskDao.fetchTasks(subtasks) { getQuery(preferences, filter!!, it) }
_tasks.value = taskDao.fetchTasks(subtasks) { getQuery(preferences, filter!!, it) }
}
private fun performPagedListQuery() {
@ -87,7 +89,7 @@ class TaskListViewModel @Inject constructor(
Timber.d("paged query: %s", query.sql)
val factory = taskDao.getTaskFactory(query)
val builder = LivePagedListBuilder(factory, PAGED_LIST_CONFIG)
val current = tasks.value
val current = _tasks.value
if (current is PagedList<*>) {
val lastKey = (current as PagedList<TaskContainer>).lastKey
if (lastKey is Int) {
@ -112,10 +114,10 @@ class TaskListViewModel @Inject constructor(
}
val value: List<TaskContainer>
get() = tasks.value ?: emptyList()
get() = _tasks.value ?: emptyList()
override fun onChanged(taskContainers: PagedList<TaskContainer>) {
tasks.value = taskContainers
_tasks.value = taskContainers
}
companion object {

@ -1,38 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:overScrollMode="never"
android:scrollbars="none" />
<LinearLayout
android:id="@+id/new_subtasks"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/recycler_view"
android:orientation="vertical">
</LinearLayout>
<TextView
android:id="@+id/add_subtask"
style="@style/TaskEditTextPrimary"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/new_subtasks"
android:clickable="true"
android:focusable="true"
android:gravity="start"
android:hint="@string/TEA_add_subtask"
android:paddingStart="0dp"
android:paddingEnd="@dimen/keyline_second"
android:textAlignment="viewStart" />
</RelativeLayout>

@ -1,40 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rowBody"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/keyline_first">
<com.todoroo.astrid.ui.CheckableImageView
android:id="@+id/completeBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:paddingStart="0dp"
android:paddingEnd="@dimen/keyline_first" />
<include
layout="@layout/control_set_clear_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true" />
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/completeBox"
android:layout_toStartOf="@id/clear"
android:background="@null"
android:focusable="true"
android:gravity="start|top"
android:hint="@string/enter_title_hint"
android:imeOptions="flagNoExtractUi"
android:inputType="textCapSentences"
android:paddingStart="0dp"
android:paddingEnd="@dimen/keyline_first"
android:singleLine="false"
android:textAlignment="viewStart"
android:textColor="?android:attr/textColorPrimary"
android:textSize="@dimen/task_edit_text_size" />
</RelativeLayout>

@ -1,38 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rowBody"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/keyline_first">
<com.todoroo.astrid.ui.CheckableImageView
android:id="@+id/completeBox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:paddingStart="0dp"
android:paddingEnd="@dimen/keyline_first" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/chip_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true" />
<TextView
android:id="@+id/title"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_toStartOf="@id/chip_group"
android:layout_toEndOf="@id/completeBox"
android:background="?attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:gravity="start|top"
android:paddingStart="0dp"
android:paddingEnd="@dimen/keyline_first"
android:textAlignment="viewStart"
android:textColor="?android:attr/textColorPrimary"
android:textSize="@dimen/task_edit_text_size" />
</RelativeLayout>
Loading…
Cancel
Save