From b4c3bec3abb8f14d6ab01e53961ef6c166c308f9 Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Wed, 13 Jul 2022 00:10:52 -0500 Subject: [PATCH] Convert subtask control set to compose --- .../main/java/com/todoroo/astrid/data/Task.kt | 15 +- .../main/java/org/tasks/compose/AlarmRow.kt | 29 -- .../java/org/tasks/compose/ClearButton.kt | 23 ++ .../main/java/org/tasks/data/GoogleTaskDao.kt | 12 +- .../org/tasks/tasklist/SubtaskViewHolder.kt | 112 ----- .../tasklist/SubtasksRecyclerAdapter.java | 105 ----- .../java/org/tasks/ui/CheckBoxProvider.kt | 31 +- .../java/org/tasks/ui/SubtaskControlSet.kt | 390 +++++++++++------- .../java/org/tasks/ui/TaskEditViewModel.kt | 6 +- .../java/org/tasks/ui/TaskListViewModel.kt | 16 +- .../main/res/layout/control_set_subtasks.xml | 38 -- .../editable_subtask_adapter_row_body.xml | 40 -- .../res/layout/subtask_adapter_row_body.xml | 38 -- 13 files changed, 299 insertions(+), 556 deletions(-) create mode 100644 app/src/main/java/org/tasks/compose/ClearButton.kt delete mode 100644 app/src/main/java/org/tasks/tasklist/SubtaskViewHolder.kt delete mode 100644 app/src/main/java/org/tasks/tasklist/SubtasksRecyclerAdapter.java delete mode 100644 app/src/main/res/layout/control_set_subtasks.xml delete mode 100644 app/src/main/res/layout/editable_subtask_adapter_row_body.xml delete mode 100644 app/src/main/res/layout/subtask_adapter_row_body.xml diff --git a/app/src/main/java/com/todoroo/astrid/data/Task.kt b/app/src/main/java/com/todoroo/astrid/data/Task.kt index 7a1859383..ef396c39b 100644 --- a/app/src/main/java/com/todoroo/astrid/data/Task.kt +++ b/app/src/main/java/com/todoroo/astrid/data/Task.kt @@ -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 diff --git a/app/src/main/java/org/tasks/compose/AlarmRow.kt b/app/src/main/java/org/tasks/compose/AlarmRow.kt index 976a0fd99..69b2b7804 100644 --- a/app/src/main/java/org/tasks/compose/AlarmRow.kt +++ b/app/src/main/java/org/tasks/compose/AlarmRow.kt @@ -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 diff --git a/app/src/main/java/org/tasks/compose/ClearButton.kt b/app/src/main/java/org/tasks/compose/ClearButton.kt new file mode 100644 index 000000000..8a6124162 --- /dev/null +++ b/app/src/main/java/org/tasks/compose/ClearButton.kt @@ -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) + ) + } +} diff --git a/app/src/main/java/org/tasks/data/GoogleTaskDao.kt b/app/src/main/java/org/tasks/data/GoogleTaskDao.kt index 934d47944..66a6fb78f 100644 --- a/app/src/main/java/org/tasks/data/GoogleTaskDao.kt +++ b/app/src/main/java/org/tasks/data/GoogleTaskDao.kt @@ -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 + @Update abstract suspend fun update(googleTask: GoogleTask) diff --git a/app/src/main/java/org/tasks/tasklist/SubtaskViewHolder.kt b/app/src/main/java/org/tasks/tasklist/SubtaskViewHolder.kt deleted file mode 100644 index 4b2b125df..000000000 --- a/app/src/main/java/org/tasks/tasklist/SubtaskViewHolder.kt +++ /dev/null @@ -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) - } -} \ No newline at end of file diff --git a/app/src/main/java/org/tasks/tasklist/SubtasksRecyclerAdapter.java b/app/src/main/java/org/tasks/tasklist/SubtasksRecyclerAdapter.java deleted file mode 100644 index 9a4066f0f..000000000 --- a/app/src/main/java/org/tasks/tasklist/SubtasksRecyclerAdapter.java +++ /dev/null @@ -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 - 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 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 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()); - } - } -} diff --git a/app/src/main/java/org/tasks/ui/CheckBoxProvider.kt b/app/src/main/java/org/tasks/ui/CheckBoxProvider.kt index 0a93332bd..31f0f84eb 100644 --- a/app/src/main/java/org/tasks/ui/CheckBoxProvider.kt +++ b/app/src/main/java/org/tasks/ui/CheckBoxProvider.kt @@ -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 + } + } } diff --git a/app/src/main/java/org/tasks/ui/SubtaskControlSet.kt b/app/src/main/java/org/tasks/ui/SubtaskControlSet.kt index a47843f28..c756b8783 100644 --- a/app/src/main/java/org/tasks/ui/SubtaskControlSet.kt +++ b/app/src/main/java/org/tasks/ui/SubtaskControlSet.kt @@ -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? -> 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(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, + 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, 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)))) - } -} \ No newline at end of file + .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)) + ) + ) + } +} diff --git a/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt b/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt index b987ab090..5cf310fc8 100644 --- a/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt +++ b/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt @@ -254,7 +254,7 @@ class TaskEditViewModel @Inject constructor( val selectedTags = MutableStateFlow(ArrayList()) - var newSubtasks = ArrayList() + var newSubtasks = MutableStateFlow(emptyList()) private lateinit var originalAlarms: List @@ -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 } diff --git a/app/src/main/java/org/tasks/ui/TaskListViewModel.kt b/app/src/main/java/org/tasks/ui/TaskListViewModel.kt index 41f830fe9..eb667fac4 100644 --- a/app/src/main/java/org/tasks/ui/TaskListViewModel.kt +++ b/app/src/main/java/org/tasks/ui/TaskListViewModel.kt @@ -29,7 +29,9 @@ class TaskListViewModel @Inject constructor( private val preferences: Preferences, private val taskDao: TaskDao) : ViewModel(), Observer> { - private var tasks = MutableLiveData>() + private var _tasks = MutableLiveData>() + val tasks: LiveData> + get() = _tasks private var filter: Filter? = null private var manualSortFilter = false private var internal: LiveData>? = 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) -> 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).lastKey if (lastKey is Int) { @@ -112,10 +114,10 @@ class TaskListViewModel @Inject constructor( } val value: List - get() = tasks.value ?: emptyList() + get() = _tasks.value ?: emptyList() override fun onChanged(taskContainers: PagedList) { - tasks.value = taskContainers + _tasks.value = taskContainers } companion object { diff --git a/app/src/main/res/layout/control_set_subtasks.xml b/app/src/main/res/layout/control_set_subtasks.xml deleted file mode 100644 index 1b1839480..000000000 --- a/app/src/main/res/layout/control_set_subtasks.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/layout/editable_subtask_adapter_row_body.xml b/app/src/main/res/layout/editable_subtask_adapter_row_body.xml deleted file mode 100644 index c00822fc0..000000000 --- a/app/src/main/res/layout/editable_subtask_adapter_row_body.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/subtask_adapter_row_body.xml b/app/src/main/res/layout/subtask_adapter_row_body.xml deleted file mode 100644 index a06375c6d..000000000 --- a/app/src/main/res/layout/subtask_adapter_row_body.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - \ No newline at end of file