Convert subtask control set to compose

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

@ -5,11 +5,7 @@ import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import androidx.annotation.IntDef import androidx.annotation.IntDef
import androidx.core.os.ParcelCompat import androidx.core.os.ParcelCompat
import androidx.room.ColumnInfo import androidx.room.*
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.Index
import androidx.room.PrimaryKey
import com.google.gson.annotations.SerializedName import com.google.gson.annotations.SerializedName
import com.todoroo.andlib.data.Table import com.todoroo.andlib.data.Table
import com.todoroo.andlib.sql.Field 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 { companion object {
const val TABLE_NAME = "tasks" const val TABLE_NAME = "tasks"
// --- table and uri // --- table and uri

@ -1,29 +1,18 @@
package org.tasks.compose package org.tasks.compose
import android.content.res.Configuration 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.isSystemInDarkTheme
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.darkColors 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.material.lightColors
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier 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.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.res.ResourcesCompat
import org.tasks.R
@Composable @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)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable @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 package org.tasks.data
import androidx.room.Dao import androidx.room.*
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 com.todoroo.astrid.data.Task import com.todoroo.astrid.data.Task
import kotlinx.coroutines.flow.Flow
import org.tasks.db.SuspendDbUtils.chunkedMap import org.tasks.db.SuspendDbUtils.chunkedMap
import org.tasks.time.DateTimeUtils.currentTimeMillis 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") @Query("SELECT * FROM google_tasks WHERE gt_task = :taskId AND gt_deleted = 0 LIMIT 1")
abstract suspend fun getByTaskId(taskId: Long): GoogleTask? 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 @Update
abstract suspend fun update(googleTask: GoogleTask) 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 org.tasks.themes.DrawableUtil
import javax.inject.Inject import javax.inject.Inject
class CheckBoxProvider @Inject constructor(@param:ActivityContext private val context: Context, private val colorProvider: ColorProvider) { class CheckBoxProvider @Inject constructor(
@param:ActivityContext private val context: Context,
fun getCheckBox(task: Task) = getCheckBox(task.isCompleted, task.isRecurring, task.priority) private val colorProvider: ColorProvider
) {
fun getCheckBox(complete: Boolean, repeating: Boolean, priority: Int) = fun getCheckBox(task: Task) = getDrawable(task.getCheckboxRes(), task.priority)
getDrawable(getDrawableRes(complete, repeating), priority)
fun getWidgetCheckBox(task: Task): Bitmap { 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)) DrawableUtil.setTint(wrapped, colorProvider.getPriorityColor(task.priority, false))
return convertToBitmap(wrapped) 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 { private fun getDrawable(@DrawableRes resId: Int, priority: Int): Drawable {
val original = context.getDrawable(resId) val original = context.getDrawable(resId)
val wrapped = original!!.mutate() val wrapped = original!!.mutate()
@ -39,10 +33,19 @@ class CheckBoxProvider @Inject constructor(@param:ActivityContext private val co
} }
private fun convertToBitmap(d: Drawable): Bitmap { 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) val canvas = Canvas(bitmap)
d.setBounds(0, 0, canvas.width, canvas.height) d.setBounds(0, 0, canvas.width, canvas.height)
d.draw(canvas) d.draw(canvas)
return bitmap 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.BroadcastReceiver
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Paint
import android.os.Bundle import android.os.Bundle
import android.text.Editable import androidx.compose.foundation.clickable
import android.view.LayoutInflater import androidx.compose.foundation.focusable
import android.view.View import androidx.compose.foundation.layout.*
import android.view.ViewGroup import androidx.compose.foundation.text.BasicTextField
import android.view.inputmethod.EditorInfo import androidx.compose.foundation.text.KeyboardActions
import android.view.inputmethod.InputMethodManager import androidx.compose.foundation.text.KeyboardOptions
import android.widget.EditText import androidx.compose.material.*
import android.widget.LinearLayout import androidx.compose.runtime.*
import androidx.core.widget.addTextChangedListener 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.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope 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.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
@ -32,77 +39,85 @@ 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
import com.todoroo.astrid.service.TaskCreator import com.todoroo.astrid.service.TaskCreator
import com.todoroo.astrid.ui.CheckableImageView
import dagger.hilt.android.AndroidEntryPoint 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.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.GoogleTask
import org.tasks.data.GoogleTaskDao import org.tasks.data.GoogleTaskDao
import org.tasks.data.TaskContainer import org.tasks.data.TaskContainer
import org.tasks.data.TaskDao.TaskCriteria.activeAndVisible import org.tasks.data.TaskDao.TaskCriteria.activeAndVisible
import org.tasks.databinding.ControlSetSubtasksBinding import org.tasks.themes.ColorProvider
import org.tasks.extensions.Context.toast import org.tasks.ui.CheckBoxProvider.Companion.getCheckboxRes
import org.tasks.tasklist.SubtaskViewHolder
import org.tasks.tasklist.SubtasksRecyclerAdapter
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class SubtaskControlSet : TaskEditControlFragment(), SubtaskViewHolder.Callbacks { class SubtaskControlSet : TaskEditControlComposeFragment() {
private lateinit var recyclerView: RecyclerView
private lateinit var newSubtaskContainer: LinearLayout
@Inject lateinit var activity: Activity @Inject lateinit var activity: Activity
@Inject lateinit var taskCompleter: TaskCompleter @Inject lateinit var taskCompleter: TaskCompleter
@Inject lateinit var localBroadcastManager: LocalBroadcastManager @Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var googleTaskDao: GoogleTaskDao @Inject lateinit var googleTaskDao: GoogleTaskDao
@Inject lateinit var taskCreator: TaskCreator @Inject lateinit var taskCreator: TaskCreator
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao
@Inject lateinit var checkBoxProvider: CheckBoxProvider @Inject lateinit var checkBoxProvider: CheckBoxProvider
@Inject lateinit var chipProvider: ChipProvider @Inject lateinit var chipProvider: ChipProvider
@Inject lateinit var eventBus: MainActivityEventBus @Inject lateinit var eventBus: MainActivityEventBus
@Inject lateinit var colorProvider: ColorProvider
private val listViewModel: TaskListViewModel by viewModels() private val listViewModel: TaskListViewModel by viewModels()
private val refreshReceiver = RefreshReceiver() private val refreshReceiver = RefreshReceiver()
private var remoteList: Filter? = null
private var googleTask: GoogleTask? = null
private lateinit var recyclerAdapter: SubtasksRecyclerAdapter
override fun createView(savedInstanceState: Bundle?) { override fun createView(savedInstanceState: Bundle?) {
viewModel.newSubtasks.forEach { addSubtask(it) } viewModel.task.takeIf { it.id > 0 }?.let {
recyclerAdapter = SubtasksRecyclerAdapter(activity, chipProvider, checkBoxProvider, this)
viewModel.task.let {
if (it.id > 0) {
recyclerAdapter.submitList(listViewModel.value)
listViewModel.setFilter(Filter("subtasks", getQueryTemplate(it))) 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
} }
} }
lifecycleScope.launch { @Composable
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) { override fun Body() {
viewModel.selectedList.collect { Column {
remoteList = it val filter = viewModel.selectedList.collectAsStateLifecycleAware().value
updateUI() 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 val icon = R.drawable.ic_subdirectory_arrow_right_black_24dp
@ -112,12 +127,6 @@ class SubtaskControlSet : TaskEditControlFragment(), SubtaskViewHolder.Callbacks
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
localBroadcastManager.registerRefreshReceiver(refreshReceiver) localBroadcastManager.registerRefreshReceiver(refreshReceiver)
lifecycleScope.launch {
viewModel.task.let {
googleTask = googleTaskDao.getByTaskId(it.id)
updateUI()
}
}
} }
override fun onPause() { override fun onPause() {
@ -125,106 +134,166 @@ class SubtaskControlSet : TaskEditControlFragment(), SubtaskViewHolder.Callbacks
localBroadcastManager.unregisterReceiver(refreshReceiver) localBroadcastManager.unregisterReceiver(refreshReceiver)
} }
private fun addSubtask() { private fun addSubtask() = lifecycleScope.launch {
if (isGoogleTaskChild) {
context?.toast(R.string.subtasks_multilevel_google_task)
} else {
lifecycleScope.launch {
val task = taskCreator.createWithValues("") val task = taskCreator.createWithValues("")
viewModel.newSubtasks.add(task) viewModel.newSubtasks.value = viewModel.newSubtasks.value.plus(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(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
private fun openSubtask(task: Task) = lifecycleScope.launch {
eventBus.emit(MainActivityEvent.OpenTask(task))
} }
false
private fun toggleSubtask(taskId: Long, collapsed: Boolean) = lifecycleScope.launch {
taskDao.setCollapsed(taskId, collapsed)
localBroadcastManager.broadcastRefresh()
} }
val completeBox: CheckableImageView = view.findViewById(R.id.completeBox)
completeBox.isChecked = task.isCompleted private fun complete(task: Task, completed: Boolean) = lifecycleScope.launch {
updateCompleteBox(task, completeBox, editText) taskCompleter.setComplete(task, completed)
completeBox.setOnClickListener { updateCompleteBox(task, completeBox, editText) }
newSubtaskContainer.addView(view)
return editText
} }
private fun updateCompleteBox(task: Task, completeBox: CheckableImageView, editText: EditText) { private inner class RefreshReceiver : BroadcastReceiver() {
val isComplete = completeBox.isChecked override fun onReceive(context: Context, intent: Intent) {
task.completionDate = if (isComplete) now() else 0 listViewModel.invalidate()
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 val isGoogleTaskChild: Boolean @Composable
get() = (remoteList is GtasksFilter override fun Icon() {
&& googleTask != null && googleTask!!.parent > 0 && googleTask!!.listId == (remoteList as GtasksFilter).remoteId) TaskEditIcon(
id = icon,
modifier = Modifier.padding(start = 16.dp, top = 20.dp, end = 20.dp, bottom = 20.dp),
)
}
private fun updateUI() { @Composable
if (isGoogleTaskChild) { fun NewSubtasks(
recyclerView.visibility = View.GONE subtasks: List<Task>,
newSubtaskContainer.visibility = View.GONE onComplete: (Task) -> Unit,
} else { onDelete: (Task) -> Unit,
recyclerView.visibility = View.VISIBLE ) {
newSubtaskContainer.visibility = View.VISIBLE subtasks.forEach { subtask ->
recyclerAdapter.setMultiLevelSubtasksEnabled(remoteList !is GtasksFilter) NewSubtaskRow(
refresh() subtask = subtask,
onComplete = onComplete,
onDelete = onDelete,
)
} }
} }
private fun refresh() { @Composable
listViewModel.invalidate() 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 openSubtask(task: Task) { @Composable
lifecycleScope.launch { fun NewSubtaskRow(
eventBus.emit(MainActivityEvent.OpenTask(task)) 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()
} }
} }
),
override fun toggleSubtask(taskId: Long, collapsed: Boolean) { singleLine = true,
lifecycleScope.launch { maxLines = Int.MAX_VALUE,
taskDao.setCollapsed(taskId, collapsed) )
localBroadcastManager.broadcastRefresh() ClearButton { onDelete(subtask) }
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
} }
} }
override fun complete(task: Task, completed: Boolean) { @Composable
lifecycleScope.launch { fun SubtaskRow(
taskCompleter.setComplete(task, completed) 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,
)
}
} }
} }
private inner class RefreshReceiver : BroadcastReceiver() { @Composable
override fun onReceive(context: Context, intent: Intent) { fun CheckBox(
refresh() 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
)
} }
} }
@ -237,10 +306,15 @@ class SubtaskControlSet : TaskEditControlFragment(), SubtaskViewHolder.Callbacks
Criterion.and( Criterion.and(
GoogleTask.PARENT.eq(task.id), GoogleTask.PARENT.eq(task.id),
GoogleTask.TASK.eq(Task.ID), GoogleTask.TASK.eq(Task.ID),
GoogleTask.DELETED.eq(0)))) GoogleTask.DELETED.eq(0)
)
)
)
.where( .where(
Criterion.and( Criterion.and(
activeAndVisible(), activeAndVisible(),
Criterion.or(Task.PARENT.eq(task.id), GoogleTask.TASK.gt(0)))) Criterion.or(Task.PARENT.eq(task.id), GoogleTask.TASK.gt(0))
)
)
} }
} }

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

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