tevm_refactor
Alex Baker 2 weeks ago
parent 93670bb9e4
commit a1402f65d0

@ -23,7 +23,7 @@ class TaskDeleterTest : InjectingTestCase() {
val task = Task()
taskDao.createNew(task)
taskDeleter.markDeleted(task)
taskDeleter.markDeleted(task.id)
assertTrue(taskDao.fetch(task.id)!!.isDeleted)
}
@ -35,7 +35,7 @@ class TaskDeleterTest : InjectingTestCase() {
)
taskDao.createNew(task)
taskDeleter.markDeleted(task)
taskDeleter.markDeleted(task.id)
assertFalse(taskDao.fetch(task.id)!!.isDeleted)
}

@ -8,6 +8,7 @@ import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.gcal.GCalHelper
import com.todoroo.astrid.service.TaskCompleter
import com.todoroo.astrid.service.TaskCreator
import com.todoroo.astrid.service.TaskDeleter
import com.todoroo.astrid.service.TaskMover
import com.todoroo.astrid.timers.TimerPlugin
@ -44,6 +45,7 @@ open class BaseTaskEditViewModelTest : InjectingTestCase() {
@Inject lateinit var tagDataDao: TagDataDao
@Inject lateinit var alarmDao: AlarmDao
@Inject lateinit var userActivityDao: UserActivityDao
@Inject lateinit var taskCreator: TaskCreator
protected lateinit var viewModel: TaskEditViewModel
@ -78,10 +80,11 @@ open class BaseTaskEditViewModelTest : InjectingTestCase() {
userActivityDao = userActivityDao,
taskAttachmentDao = db.taskAttachmentDao,
alarmDao = db.alarmDao,
taskCreator = taskCreator,
)
}
protected fun save(): Boolean = runBlocking(Dispatchers.Main) {
protected fun save(): Task? = runBlocking(Dispatchers.Main) {
viewModel.save()
}
}

@ -4,7 +4,9 @@ import com.natpryce.makeiteasy.MakeItEasy
import com.todoroo.astrid.data.Task
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker
@ -16,29 +18,28 @@ class PriorityTests : BaseTaskEditViewModelTest() {
fun changePriorityCausesChange() {
setup(TaskMaker.newTask(MakeItEasy.with(TaskMaker.PRIORITY, Task.Priority.HIGH)))
viewModel.priority.value = Task.Priority.MEDIUM
viewModel.setPriority(Task.Priority.MEDIUM)
Assert.assertTrue(viewModel.hasChanges())
assertTrue(viewModel.hasChanges())
}
@Test
fun applyPriorityChange() {
val task = TaskMaker.newTask(MakeItEasy.with(TaskMaker.PRIORITY, Task.Priority.HIGH))
setup(task)
viewModel.priority.value = Task.Priority.MEDIUM
setup(TaskMaker.newTask(MakeItEasy.with(TaskMaker.PRIORITY, Task.Priority.HIGH)))
viewModel.setPriority(Task.Priority.MEDIUM)
save()
val task = save()
Assert.assertEquals(Task.Priority.MEDIUM, task.priority)
assertEquals(Task.Priority.MEDIUM, task?.priority)
}
@Test
fun noChangeWhenRevertingPriority() {
setup(TaskMaker.newTask(MakeItEasy.with(TaskMaker.PRIORITY, Task.Priority.HIGH)))
viewModel.priority.value = Task.Priority.MEDIUM
viewModel.priority.value = Task.Priority.HIGH
viewModel.setPriority(Task.Priority.MEDIUM)
viewModel.setPriority(Task.Priority.HIGH)
Assert.assertFalse(viewModel.hasChanges())
assertFalse(viewModel.hasChanges())
}
}

@ -4,7 +4,9 @@ import com.todoroo.astrid.data.Task
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.*
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.data.Alarm
import org.tasks.data.Alarm.Companion.whenOverdue
@ -17,9 +19,7 @@ import org.tasks.time.DateTimeUtils.currentTimeMillis
class ReminderTests : BaseTaskEditViewModelTest() {
@Test
fun whenStartReminder() = runBlocking {
val task = newTask()
task.defaultReminders(Task.NOTIFY_AT_START)
setup(task)
setup(newTask().apply { defaultReminders(Task.NOTIFY_AT_START) })
viewModel.setStartDate(
Task.createDueDate(
@ -28,19 +28,17 @@ class ReminderTests : BaseTaskEditViewModelTest() {
)
)
save()
val task = save()
assertEquals(
listOf(Alarm(1, 0, Alarm.TYPE_REL_START).apply { id = 1 }),
alarmDao.getAlarms(task.id)
alarmDao.getAlarms(task!!.id)
)
}
@Test
fun whenDueReminder() = runBlocking {
val task = newTask()
task.defaultReminders(Task.NOTIFY_AT_DEADLINE)
setup(task)
setup(newTask().apply { defaultReminders(Task.NOTIFY_AT_DEADLINE) })
viewModel.setDueDate(
Task.createDueDate(
@ -49,19 +47,17 @@ class ReminderTests : BaseTaskEditViewModelTest() {
)
)
save()
val task = save()
assertEquals(
listOf(Alarm(1, 0, Alarm.TYPE_REL_END).apply { id = 1 }),
alarmDao.getAlarms(task.id)
alarmDao.getAlarms(task!!.id)
)
}
@Test
fun whenOverDueReminder() = runBlocking {
val task = newTask()
task.defaultReminders(Task.NOTIFY_AFTER_DEADLINE)
setup(task)
setup(newTask().apply { defaultReminders(Task.NOTIFY_AFTER_DEADLINE) })
viewModel.setDueDate(
Task.createDueDate(
@ -70,63 +66,59 @@ class ReminderTests : BaseTaskEditViewModelTest() {
)
)
save()
val task = save()
assertEquals(
listOf(whenOverdue(1).apply { id = 1 }),
alarmDao.getAlarms(task.id)
alarmDao.getAlarms(task!!.id)
)
}
@Test
fun ringFiveTimes() = runBlocking {
val task = newTask()
setup(task)
setup(newTask())
viewModel.ringFiveTimes = true
viewModel.setRingMode(1)
save()
val task = save()
assertTrue(taskDao.fetch(task.id)!!.isNotifyModeFive)
assertTrue(taskDao.fetch(task!!.id)!!.isNotifyModeFive)
}
@Test
fun ringNonstop() = runBlocking {
val task = newTask()
setup(task)
setup(newTask())
viewModel.ringNonstop = true
viewModel.setRingMode(2)
save()
val task = save()
assertTrue(taskDao.fetch(task.id)!!.isNotifyModeNonstop)
assertTrue(taskDao.fetch(task!!.id)!!.isNotifyModeNonstop)
}
@Test
fun ringFiveTimesCantRingNonstop() = runBlocking {
val task = newTask()
setup(task)
setup(newTask())
viewModel.ringNonstop = true
viewModel.ringFiveTimes = true
viewModel.setRingMode(2)
viewModel.setRingMode(1)
save()
val task = save()
assertFalse(taskDao.fetch(task.id)!!.isNotifyModeNonstop)
assertFalse(taskDao.fetch(task!!.id)!!.isNotifyModeNonstop)
assertTrue(taskDao.fetch(task.id)!!.isNotifyModeFive)
}
@Test
fun ringNonStopCantRingFiveTimes() = runBlocking {
val task = newTask()
setup(task)
setup(newTask())
viewModel.ringFiveTimes = true
viewModel.ringNonstop = true
viewModel.setRingMode(1)
viewModel.setRingMode(2)
save()
val task = save()
assertFalse(taskDao.fetch(task.id)!!.isNotifyModeFive)
assertFalse(taskDao.fetch(task!!.id)!!.isNotifyModeFive)
assertTrue(taskDao.fetch(task.id)!!.isNotifyModeNonstop)
}
}

@ -5,6 +5,8 @@ import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.injection.ProductionModule
@ -24,7 +26,7 @@ class TaskEditViewModelTest : BaseTaskEditViewModelTest() {
fun dontSaveTaskWithoutChanges() = runBlocking {
setup(newTask())
assertFalse(save())
assertNull(save())
assertTrue(taskDao.getAll().isEmpty())
}
@ -33,10 +35,10 @@ class TaskEditViewModelTest : BaseTaskEditViewModelTest() {
fun dontSaveTaskTwice() = runBlocking {
setup(newTask())
viewModel.priority.value = Task.Priority.HIGH
viewModel.setPriority(Task.Priority.HIGH)
assertTrue(save())
assertNotNull(viewModel.save())
assertFalse(viewModel.save())
assertNull(viewModel.save())
}
}

@ -1,6 +1,5 @@
package org.tasks.ui.editviewmodel
import com.natpryce.makeiteasy.MakeItEasy.with
import com.todoroo.astrid.data.Task.Priority.Companion.HIGH
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
@ -9,7 +8,6 @@ import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker
import org.tasks.makers.TaskMaker.newTask
@UninstallModules(ProductionModule::class)
@ -19,27 +17,19 @@ class TitleTests : BaseTaskEditViewModelTest() {
fun changeTitleCausesChange() {
setup(newTask())
viewModel.title = "Test"
viewModel.setTitle("Test")
assertTrue(viewModel.hasChanges())
}
@Test
fun saveWithEmptyTitle() = runBlocking {
val task = newTask()
setup(task)
viewModel.priority.value = HIGH
save()
setup(newTask())
assertEquals("(No title)", taskDao.fetch(task.id)!!.title)
}
viewModel.setPriority(HIGH)
@Test
fun newTaskPrepopulatedWithTitleHasChanges() {
setup(newTask(with(TaskMaker.TITLE, "some title")))
val task = save()
assertTrue(viewModel.hasChanges())
assertEquals("(No title)", taskDao.fetch(task!!.id)!!.title)
}
}

@ -25,8 +25,10 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.Divider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
@ -38,14 +40,19 @@ import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.os.BundleCompat
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.appbar.AppBarLayout.Behavior.DragCallback
import com.google.android.material.composethemeadapter.MdcTheme
import com.todoroo.andlib.sql.Criterion
import com.todoroo.andlib.sql.QueryTemplate
import com.todoroo.andlib.utility.AndroidUtilities.atLeastOreoMR1
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.api.Filter
import com.todoroo.astrid.api.FilterImpl
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.files.FilesControlSet
@ -57,7 +64,6 @@ import com.todoroo.astrid.ui.StartDateControlSet
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
@ -71,9 +77,11 @@ import org.tasks.compose.edit.DueDateRow
import org.tasks.compose.edit.InfoRow
import org.tasks.compose.edit.ListRow
import org.tasks.compose.edit.PriorityRow
import org.tasks.compose.edit.SubtaskRow
import org.tasks.data.Alarm
import org.tasks.data.Location
import org.tasks.data.TagData
import org.tasks.data.TaskDao.TaskCriteria.activeAndVisible
import org.tasks.data.UserActivityDao
import org.tasks.databinding.FragmentTaskEditBinding
import org.tasks.databinding.TaskEditCalendarBinding
@ -82,7 +90,6 @@ import org.tasks.databinding.TaskEditLocationBinding
import org.tasks.databinding.TaskEditRemindersBinding
import org.tasks.databinding.TaskEditRepeatBinding
import org.tasks.databinding.TaskEditStartDateBinding
import org.tasks.databinding.TaskEditSubtasksBinding
import org.tasks.databinding.TaskEditTagsBinding
import org.tasks.databinding.TaskEditTimerBinding
import org.tasks.date.DateTimeUtils.newDateTime
@ -99,18 +106,19 @@ import org.tasks.fragments.TaskEditControlSetFragmentManager.Companion.TAG_DESCR
import org.tasks.fragments.TaskEditControlSetFragmentManager.Companion.TAG_DUE_DATE
import org.tasks.fragments.TaskEditControlSetFragmentManager.Companion.TAG_LIST
import org.tasks.fragments.TaskEditControlSetFragmentManager.Companion.TAG_PRIORITY
import org.tasks.fragments.TaskEditControlSetFragmentManager.Companion.TAG_SUBTASKS
import org.tasks.markdown.MarkdownProvider
import org.tasks.notifications.NotificationManager
import org.tasks.play.PlayServices
import org.tasks.preferences.Preferences
import org.tasks.tasklist.SectionedDataSource
import org.tasks.ui.CalendarControlSet
import org.tasks.ui.ChipProvider
import org.tasks.ui.LocationControlSet
import org.tasks.ui.SubtaskControlSet
import org.tasks.ui.TaskEditEvent
import org.tasks.ui.TaskEditEventBus
import org.tasks.ui.TaskEditViewModel
import org.tasks.ui.TaskEditViewModel.Companion.stripCarriageReturns
import org.tasks.ui.TaskListViewModel
import java.time.format.FormatStyle
import java.util.Locale
import javax.inject.Inject
@ -134,6 +142,7 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
@Inject lateinit var playServices: PlayServices
val editViewModel: TaskEditViewModel by viewModels()
private val mainViewModel: MainActivityViewModel by activityViewModels()
lateinit var binding: FragmentTaskEditBinding
private var showKeyboard = false
private val beastMode =
@ -161,7 +170,7 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
binding = FragmentTaskEditBinding.inflate(inflater)
val view: View = binding.root
val model = editViewModel.task
val model = editViewModel.originalTask
val toolbar = binding.toolbar
toolbar.navigationIcon = AppCompatResources.getDrawable(
context,
@ -209,7 +218,7 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
val textWatcher = markdownProvider.markdown(preferences.linkify).textWatcher(title)
title.addTextChangedListener(
onTextChanged = { _, _, _, _ ->
editViewModel.title = title.text.toString().trim { it <= ' ' }
editViewModel.setTitle(title.text.toString().trim { it <= ' ' })
},
afterTextChanged = {
textWatcher?.invoke(it)
@ -237,17 +246,17 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
editViewModel.isReadOnly
) {
binding.fab.visibility = View.INVISIBLE
} else if (editViewModel.completed) {
} else if (editViewModel.state.value.task.isCompleted) {
title.paintFlags = title.paintFlags or Paint.STRIKE_THRU_TEXT_FLAG
binding.fab.setImageResource(R.drawable.ic_outline_check_box_outline_blank_24px)
}
binding.fab.setOnClickListener {
if (editViewModel.completed) {
editViewModel.completed = false
if (editViewModel.state.value.task.isCompleted) {
editViewModel.setCompleted(false)
title.paintFlags = title.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv()
binding.fab.setImageResource(R.drawable.ic_outline_check_box_24px)
} else {
editViewModel.completed = true
editViewModel.setCompleted(true)
lifecycleScope.launch {
save()
}
@ -283,19 +292,15 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
TAG_DESCRIPTION -> DescriptionRow()
TAG_LIST -> ListRow()
TAG_CREATION -> CreationRow()
TAG_SUBTASKS -> SubtaskRow()
CalendarControlSet.TAG -> AndroidViewBinding(TaskEditCalendarBinding::inflate)
StartDateControlSet.TAG -> AndroidViewBinding(
TaskEditStartDateBinding::inflate
)
ReminderControlSet.TAG -> AndroidViewBinding(
TaskEditRemindersBinding::inflate
)
StartDateControlSet.TAG -> AndroidViewBinding(TaskEditStartDateBinding::inflate)
ReminderControlSet.TAG -> AndroidViewBinding(TaskEditRemindersBinding::inflate)
LocationControlSet.TAG -> AndroidViewBinding(TaskEditLocationBinding::inflate)
FilesControlSet.TAG -> AndroidViewBinding(TaskEditFilesBinding::inflate)
TimerControlSet.TAG -> AndroidViewBinding(TaskEditTimerBinding::inflate)
TagsControlSet.TAG -> AndroidViewBinding(TaskEditTagsBinding::inflate)
RepeatControlSet.TAG -> AndroidViewBinding(TaskEditRepeatBinding::inflate)
SubtaskControlSet.TAG -> AndroidViewBinding(TaskEditSubtasksBinding::inflate)
else -> throw IllegalArgumentException("Unknown row: $tag")
}
Divider(modifier = Modifier.fillMaxWidth())
@ -308,7 +313,7 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
}
}
childFragmentManager.setFilterPickerResultListener(this) { filter ->
editViewModel.selectedList.update { filter }
editViewModel.setFilter(filter)
}
return view
}
@ -329,7 +334,7 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
private suspend fun process(event: TaskEditEvent) {
when (event) {
is TaskEditEvent.Discard ->
if (event.id == editViewModel.task.id) {
if (event.id == editViewModel.originalTask.id) {
editViewModel.discard()
}
}
@ -425,8 +430,9 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
}
REQUEST_CODE_PICK_CALENDAR -> {
if (resultCode == Activity.RESULT_OK) {
editViewModel.selectedCalendar.value =
editViewModel.setCalendar(
data!!.getStringExtra(CalendarPicker.EXTRA_CALENDAR_ID)
)
}
}
else -> super.onActivityResult(requestCode, resultCode, data)
@ -435,32 +441,32 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
@Composable
private fun DueDateRow() {
val dueDate = editViewModel.dueDate.collectAsStateLifecycleAware().value
val state = editViewModel.state.collectAsStateLifecycleAware().value
DueDateRow(
dueDate = if (dueDate == 0L) {
dueDate = if (state.task.dueDate == 0L) {
null
} else {
DateUtilities.getRelativeDateTime(
LocalContext.current,
dueDate,
state.task.dueDate,
locale,
FormatStyle.FULL,
preferences.alwaysDisplayFullDate,
false
)
},
overdue = dueDate.isOverdue,
overdue = state.task.dueDate.isOverdue,
onClick = {
DateTimePicker
.newDateTimePicker(
target = this@TaskEditFragment,
rc = REQUEST_DATE,
current = editViewModel.dueDate.value,
current = state.task.dueDate,
autoClose = preferences.getBoolean(
R.string.p_auto_dismiss_datetime_edit_screen,
false
),
hideNoDate = editViewModel.recurrence.value?.isNotBlank() == true,
hideNoDate = state.task.recurrence?.isNotBlank() == true,
)
.show(parentFragmentManager, FRAG_TAG_DATE_PICKER)
}
@ -470,8 +476,8 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
@Composable
private fun PriorityRow() {
PriorityRow(
priority = editViewModel.priority.collectAsStateLifecycleAware().value,
onChangePriority = { editViewModel.priority.value = it },
priority = editViewModel.state.collectAsStateLifecycleAware().value.task.priority,
onChangePriority = { editViewModel.setPriority(it) },
desaturate = preferences.desaturateDarkMode,
)
}
@ -479,8 +485,8 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
@Composable
private fun DescriptionRow() {
DescriptionRow(
text = editViewModel.description.stripCarriageReturns(),
onChanged = { text -> editViewModel.description = text.toString().trim { it <= ' ' } },
text = editViewModel.state.collectAsStateLifecycleAware().value.task.notes,
onChanged = { text -> editViewModel.setDescription(text.toString().trim { it <= ' ' }) },
linkify = if (preferences.linkify) linkify else null,
markdownProvider = markdownProvider,
)
@ -488,7 +494,7 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
@Composable
private fun ListRow() {
val list = editViewModel.selectedList.collectAsStateLifecycleAware().value
val list = editViewModel.state.collectAsStateLifecycleAware().value.filter
ListRow(
list = list,
colorProvider = { chipProvider.getColor(it) },
@ -504,19 +510,64 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
@Composable
fun CreationRow() {
val task = editViewModel.state.collectAsStateLifecycleAware().value.task
InfoRow(
creationDate = editViewModel.creationDate,
modificationDate = editViewModel.modificationDate,
completionDate = editViewModel.completionDate,
creationDate = task.creationDate,
modificationDate = task.modificationDate,
completionDate = task.completionDate,
locale = locale,
)
}
@Composable
fun SubtaskRow(
listViewModel: TaskListViewModel = viewModel(),
) {
val task = editViewModel.state.collectAsStateLifecycleAware().value.task
var loaded by remember { mutableStateOf(false) }
LaunchedEffect(task) {
if (task.id > 0) {
listViewModel.setFilter(
FilterImpl(
"subtasks",
QueryTemplate()
.where(Criterion.and(activeAndVisible(), Task.PARENT.eq(task.id)))
.toString()
)
)
loaded = true
}
}
val state = editViewModel.state.collectAsStateLifecycleAware().value
SubtaskRow(
originalFilter = editViewModel.originalFilter,
filter = state.filter,
hasParent = state.task.parent > 0,
desaturate = preferences.desaturateDarkMode,
existingSubtasks = if (!loaded || editViewModel.isNew) {
TaskListViewModel.TasksResults.Results(SectionedDataSource())
} else {
listViewModel.state.collectAsStateLifecycleAware().value.tasks
},
newSubtasks = state.newSubtasks,
openSubtask = { mainViewModel.setTask(it) },
completeExistingSubtask = { task, completed ->
editViewModel.setCompleted(task, completed)
},
toggleSubtask = { taskId, collapsed ->
editViewModel.toggleSubtask(taskId, collapsed)
},
addSubtask = { editViewModel.addSubtask() },
completeNewSubtask = { editViewModel.toggleNewSubtaskCompleted(it) },
deleteSubtask = { editViewModel.removeNewSubtask(it) }
)
}
@Composable
fun Comments() {
CommentsRow(
comments = userActivityDao
.watchComments(editViewModel.task.uuid)
.watchComments(editViewModel.originalTask.uuid)
.collectAsStateLifecycleAware(emptyList())
.value,
deleteComment = {
@ -566,7 +617,6 @@ class TaskEditFragment : Fragment(), Toolbar.OnMenuItemClickListener {
taskEditFragment.arguments = arguments
return taskEditFragment
}
fun Modifier.gesturesDisabled(disabled: Boolean = true) =
if (disabled) {
pointerInput(Unit) {

@ -15,7 +15,6 @@ import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.lifecycleScope
import com.google.android.material.composethemeadapter.MdcTheme
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.tasks.R
import org.tasks.Strings
@ -35,7 +34,7 @@ class FilesControlSet : TaskEditControlFragment() {
@Inject lateinit var preferences: Preferences
override fun createView(savedInstanceState: Bundle?) {
val task = viewModel.task
val task = viewModel.originalTask
if (savedInstanceState == null) {
if (task.hasTransitory(TaskAttachment.KEY)) {
for (uri in (task.getTransitory<ArrayList<Uri>>(TaskAttachment.KEY))!!) {
@ -49,8 +48,9 @@ class FilesControlSet : TaskEditControlFragment() {
(parent as ComposeView).apply {
setContent {
MdcTheme {
val state = viewModel.state.collectAsStateLifecycleAware().value
AttachmentRow(
attachments = viewModel.selectedAttachments.collectAsStateLifecycleAware().value,
attachments = state.attachments,
openAttachment = {
FileHelper.startActionView(
requireActivity(),
@ -94,9 +94,7 @@ class FilesControlSet : TaskEditControlFragment() {
}
private fun deleteAttachment(attachment: TaskAttachment) {
viewModel.selectedAttachments.update {
it.minus(attachment)
}
viewModel.deleteAttachment(attachment)
}
private fun copyToAttachmentDirectory(input: Uri?) {
@ -110,11 +108,7 @@ class FilesControlSet : TaskEditControlFragment() {
)
lifecycleScope.launch {
taskAttachmentDao.insert(attachment)
viewModel.selectedAttachments.update {
it.plus(
taskAttachmentDao.getAttachment(attachment.remoteId) ?: return@launch
)
}
viewModel.addAttachment(taskAttachmentDao.getAttachment(attachment.remoteId) ?: return@launch)
}
}

@ -7,9 +7,9 @@ package com.todoroo.astrid.repeats
import android.app.Activity.RESULT_OK
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.lifecycleScope
import com.google.android.material.composethemeadapter.MdcTheme
@ -40,11 +40,7 @@ class RepeatControlSet : TaskEditControlFragment() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_RECURRENCE) {
if (resultCode == RESULT_OK) {
val result = data?.getStringExtra(BasicRecurrenceDialog.EXTRA_RRULE)
viewModel.recurrence.value = result
if (result?.isNotBlank() == true && viewModel.dueDate.value == 0L) {
viewModel.setDueDate(DateTime().startOfDay().millis)
}
viewModel.setRecurrence(data?.getStringExtra(BasicRecurrenceDialog.EXTRA_RRULE))
}
} else {
super.onActivityResult(requestCode, resultCode, data)
@ -52,12 +48,13 @@ class RepeatControlSet : TaskEditControlFragment() {
}
private fun onDueDateChanged() {
viewModel.recurrence.value?.takeIf { it.isNotBlank() }?.let { recurrence ->
val state = viewModel.state.value
state.task.recurrence?.takeIf { it.isNotBlank() }?.let { recurrence ->
val recur = newRecur(recurrence)
if (recur.frequency == Recur.Frequency.MONTHLY && recur.dayList.isNotEmpty()) {
val weekdayNum = recur.dayList[0]
val dateTime =
DateTime(this.viewModel.dueDate.value.let { if (it > 0) it else currentTimeMillis() })
DateTime(state.task.dueDate.let { if (it > 0) it else currentTimeMillis() })
val num: Int
val dayOfWeekInMonth = dateTime.dayOfWeekInMonth
num = if (weekdayNum.offset == -1 || dayOfWeekInMonth == 5) {
@ -69,15 +66,7 @@ class RepeatControlSet : TaskEditControlFragment() {
it.clear()
it.add(WeekDay(dateTime.weekDay, num))
}
viewModel.recurrence.value = recur.toString()
}
}
}
override fun createView(savedInstanceState: Bundle?) {
lifecycleScope.launchWhenResumed {
viewModel.dueDate.collect {
onDueDateChanged()
viewModel.setRecurrence(recur.toString())
}
}
}
@ -86,14 +75,17 @@ class RepeatControlSet : TaskEditControlFragment() {
(parent as ComposeView).apply {
setContent {
MdcTheme {
val state = viewModel.state.collectAsStateLifecycleAware().value
LaunchedEffect(state.task.dueDate) {
onDueDateChanged()
}
RepeatRow(
recurrence = viewModel.recurrence.collectAsStateLifecycleAware().value?.let {
repeatRuleToString.toString(it)
},
repeatAfterCompletion = viewModel.repeatAfterCompletion.collectAsStateLifecycleAware().value,
recurrence = state.task.recurrence?.let { repeatRuleToString.toString(it) },
repeatAfterCompletion = state.task.repeatAfterCompletion(),
onClick = {
lifecycleScope.launch {
val accountType = viewModel.selectedList.value
val accountType = state
.filter
.let {
when (it) {
is CaldavFilter -> it.account
@ -107,14 +99,14 @@ class RepeatControlSet : TaskEditControlFragment() {
BasicRecurrenceDialog.newBasicRecurrenceDialog(
target = this@RepeatControlSet,
rc = REQUEST_RECURRENCE,
rrule = viewModel.recurrence.value,
dueDate = viewModel.dueDate.value,
rrule = state.task.recurrence,
dueDate = state.task.dueDate,
accountType = accountType,
)
.show(parentFragmentManager, FRAG_TAG_BASIC_RECURRENCE)
}
},
onRepeatFromChanged = { viewModel.repeatAfterCompletion.value = it }
onRepeatFromChanged = { viewModel.setRepeatAfterCompletion(it) }
)
}
}

@ -39,7 +39,7 @@ class TaskDeleter @Inject constructor(
private val locationDao: LocationDao,
) {
suspend fun markDeleted(item: Task) = markDeleted(listOf(item.id))
suspend fun markDeleted(taskId: Long) = markDeleted(listOf(taskId))
suspend fun markDeleted(taskIds: List<Long>): List<Task> = withContext(NonCancellable) {
val ids = taskIds

@ -7,7 +7,6 @@ import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import com.google.android.material.composethemeadapter.MdcTheme
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.update
import org.tasks.R
import org.tasks.compose.collectAsStateLifecycleAware
import org.tasks.compose.edit.TagsRow
@ -22,7 +21,7 @@ class TagsControlSet : TaskEditControlFragment() {
private fun onRowClick() {
val intent = Intent(context, TagPickerActivity::class.java)
intent.putParcelableArrayListExtra(TagPickerActivity.EXTRA_SELECTED, viewModel.selectedTags.value)
intent.putParcelableArrayListExtra(TagPickerActivity.EXTRA_SELECTED, ArrayList(viewModel.state.value.tags))
startActivityForResult(intent, REQUEST_TAG_PICKER_ACTIVITY)
}
@ -30,13 +29,12 @@ class TagsControlSet : TaskEditControlFragment() {
(parent as ComposeView).apply {
setContent {
MdcTheme {
val state = viewModel.state.collectAsStateLifecycleAware().value
TagsRow(
tags = viewModel.selectedTags.collectAsStateLifecycleAware().value,
tags = state.tags,
colorProvider = { chipProvider.getColor(it) },
onClick = this@TagsControlSet::onRowClick,
onClear = { tag ->
viewModel.selectedTags.update { ArrayList(it.minus(tag)) }
},
onClear = { viewModel.removeTag(it) },
)
}
}
@ -47,9 +45,10 @@ class TagsControlSet : TaskEditControlFragment() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_TAG_PICKER_ACTIVITY) {
if (resultCode == Activity.RESULT_OK && data != null) {
viewModel.selectedTags.value =
viewModel.setTags(
data.getParcelableArrayListExtra(TagPickerActivity.EXTRA_SELECTED)
?: ArrayList()
?: emptyList()
)
}
} else {
super.onActivityResult(requestCode, resultCode, data)

@ -49,8 +49,8 @@ class TimerControlSet : TaskEditControlFragment() {
dialogView = activity.layoutInflater.inflate(R.layout.control_set_timers_dialog, null)
estimated = TimeDurationControlSet(activity, dialogView, R.id.estimatedDuration, theme)
elapsed = TimeDurationControlSet(activity, dialogView, R.id.elapsedDuration, theme)
estimated.setTimeDuration(viewModel.estimatedSeconds.value)
elapsed.setTimeDuration(viewModel.elapsedSeconds.value)
estimated.setTimeDuration(viewModel.state.value.task.estimatedSeconds)
elapsed.setTimeDuration(viewModel.state.value.task.elapsedSeconds)
}
private fun onRowClick() {
@ -65,8 +65,10 @@ class TimerControlSet : TaskEditControlFragment() {
.newDialog()
.setView(dialogView)
.setPositiveButton(R.string.ok) { _, _ ->
viewModel.estimatedSeconds.value = estimated.timeDurationInSeconds
viewModel.elapsedSeconds.value = elapsed.timeDurationInSeconds
viewModel.setTimer(
estimated = estimated.timeDurationInSeconds,
elapsed = elapsed.timeDurationInSeconds,
)
}
.setOnCancelListener {}
.create()
@ -76,12 +78,12 @@ class TimerControlSet : TaskEditControlFragment() {
lifecycleScope.launch {
if (timerActive()) {
val task = stopTimer()
viewModel.elapsedSeconds.value = task.elapsedSeconds
viewModel.setElapsed(task.elapsedSeconds)
elapsed.setTimeDuration(task.elapsedSeconds)
viewModel.timerStarted.value = 0
viewModel.setTimerStarted(0L)
} else {
val task = startTimer()
viewModel.timerStarted.value = task.timerStart
viewModel.setTimerStarted(task.timerStart)
}
}
}
@ -90,10 +92,11 @@ class TimerControlSet : TaskEditControlFragment() {
(parent as ComposeView).apply {
setContent {
MdcTheme {
val state = viewModel.state.collectAsStateLifecycleAware().value
TimerRow(
started = viewModel.timerStarted.collectAsStateLifecycleAware().value,
estimated = viewModel.estimatedSeconds.collectAsStateLifecycleAware().value,
elapsed = viewModel.elapsedSeconds.collectAsStateLifecycleAware().value,
started = state.task.timerStart,
estimated = state.task.estimatedSeconds,
elapsed = state.task.elapsedSeconds,
timerClicked = this@TimerControlSet::timerClicked,
onClick = this@TimerControlSet::onRowClick,
)
@ -103,10 +106,10 @@ class TimerControlSet : TaskEditControlFragment() {
override fun controlId() = TAG
private fun timerActive() = viewModel.timerStarted.value > 0
private fun timerActive() = viewModel.state.value.task.timerStart > 0
private suspend fun stopTimer(): Task {
val model = viewModel.task
val model = viewModel.originalTask
timerPlugin.stopTimer(model)
val elapsedTime = DateUtils.formatElapsedTime(model.elapsedSeconds.toLong())
viewModel.addComment(String.format(
@ -120,7 +123,7 @@ class TimerControlSet : TaskEditControlFragment() {
}
private suspend fun startTimer(): Task {
val model = viewModel.task
val model = viewModel.originalTask
timerPlugin.startTimer(model)
viewModel.addComment(String.format(
"%s %s",

@ -52,7 +52,7 @@ class TimerPlugin @Inject constructor(
if (task.timerStart > 0) {
val newElapsed = ((DateUtilities.now() - task.timerStart) / 1000L).toInt()
task.timerStart = 0L
task.elapsedSeconds = task.elapsedSeconds + newElapsed
task.elapsedSeconds += newElapsed
}
}
taskDao.update(task)

@ -3,15 +3,11 @@ package com.todoroo.astrid.ui
import android.Manifest
import android.app.Activity
import android.app.Activity.RESULT_OK
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.ComposeView
import com.google.accompanist.permissions.ExperimentalPermissionsApi
@ -19,6 +15,8 @@ import com.google.accompanist.permissions.PermissionStatus
import com.google.accompanist.permissions.rememberPermissionState
import com.google.android.material.composethemeadapter.MdcTheme
import com.todoroo.andlib.utility.AndroidUtilities
import com.todoroo.astrid.data.Task.Companion.NOTIFY_MODE_FIVE
import com.todoroo.astrid.data.Task.Companion.NOTIFY_MODE_NONSTOP
import dagger.hilt.android.AndroidEntryPoint
import org.tasks.R
import org.tasks.activities.DateAndTimePickerActivity
@ -31,7 +29,7 @@ import org.tasks.dialogs.DialogBuilder
import org.tasks.dialogs.MyTimePickerDialog
import org.tasks.scheduling.NotificationSchedulerIntentService
import org.tasks.ui.TaskEditControlFragment
import java.util.*
import java.util.Locale
import javax.inject.Inject
@AndroidEntryPoint
@ -40,44 +38,19 @@ class ReminderControlSet : TaskEditControlFragment() {
@Inject lateinit var dialogBuilder: DialogBuilder
@Inject lateinit var locale: Locale
private val ringMode = mutableStateOf(0)
override fun createView(savedInstanceState: Bundle?) {
when {
viewModel.ringNonstop -> setRingMode(2)
viewModel.ringFiveTimes -> setRingMode(1)
else -> setRingMode(0)
}
}
private fun onClickRingType() {
val modes = resources.getStringArray(R.array.reminder_ring_modes)
val ringMode = when {
viewModel.ringNonstop -> 2
viewModel.ringFiveTimes -> 1
else -> 0
}
dialogBuilder
.newDialog()
.setSingleChoiceItems(modes, ringMode) { dialog: DialogInterface, which: Int ->
setRingMode(which)
dialog.dismiss()
}
.show()
}
private fun setRingMode(ringMode: Int) {
viewModel.ringNonstop = ringMode == 2
viewModel.ringFiveTimes = ringMode == 1
this.ringMode.value = ringMode
}
@OptIn(ExperimentalPermissionsApi::class)
override fun bind(parent: ViewGroup?): View =
(parent as ComposeView).apply {
setContent {
MdcTheme {
val ringMode by remember { this@ReminderControlSet.ringMode }
val state = viewModel.state.collectAsStateLifecycleAware().value
val ringMode = remember (state.task.ringFlags) {
when (state.task.ringFlags) {
NOTIFY_MODE_NONSTOP -> 2
NOTIFY_MODE_FIVE -> 1
else -> 0
}
}
val notificationPermissions = if (AndroidUtilities.atLeastTiramisu()) {
rememberPermissionState(
Manifest.permission.POST_NOTIFICATIONS,
@ -102,7 +75,7 @@ class ReminderControlSet : TaskEditControlFragment() {
}
AlarmRow(
locale = locale,
alarms = viewModel.selectedAlarms.collectAsStateLifecycleAware().value,
alarms = state.alarms,
permissionStatus = notificationPermissions?.status
?: PermissionStatus.Granted,
launchPermissionRequest = {
@ -110,7 +83,16 @@ class ReminderControlSet : TaskEditControlFragment() {
},
ringMode = ringMode,
addAlarm = viewModel::addAlarm,
openRingType = this@ReminderControlSet::onClickRingType,
openRingType = {
val modes = resources.getStringArray(R.array.reminder_ring_modes)
dialogBuilder
.newDialog()
.setSingleChoiceItems(modes, ringMode) { dialog, which ->
viewModel.setRingMode(which)
dialog.dismiss()
}
.show()
},
deleteAlarm = viewModel::removeAlarm,
pickDateAndTime = { replace ->
val timestamp = replace?.takeIf { it.type == TYPE_DATE_TIME }?.time

@ -5,9 +5,9 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.ViewGroup
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import com.google.android.material.composethemeadapter.MdcTheme
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.andlib.utility.DateUtilities.getTimeString
@ -24,7 +24,7 @@ import org.tasks.dialogs.StartDatePicker.Companion.NO_TIME
import org.tasks.preferences.Preferences
import org.tasks.ui.TaskEditControlFragment
import java.time.format.FormatStyle
import java.util.*
import java.util.Locale
import javax.inject.Inject
@AndroidEntryPoint
@ -36,12 +36,8 @@ class StartDateControlSet : TaskEditControlFragment() {
override fun createView(savedInstanceState: Bundle?) {
if (savedInstanceState == null) {
vm.init(viewModel.dueDate.value, viewModel.startDate.value, viewModel.isNew)
}
lifecycleScope.launchWhenResumed {
viewModel.dueDate.collect {
applySelected()
}
val task = viewModel.state.value.task
vm.init(task.dueDate, task.hideUntil, task.isNew)
}
}
@ -49,13 +45,17 @@ class StartDateControlSet : TaskEditControlFragment() {
(parent as ComposeView).apply {
setContent {
MdcTheme {
val state = viewModel.state.collectAsStateLifecycleAware().value
LaunchedEffect(state.task.dueDate) {
applySelected()
}
val selectedDay = vm.selectedDay.collectAsStateLifecycleAware().value
val selectedTime = vm.selectedTime.collectAsStateLifecycleAware().value
StartDateRow(
startDate = viewModel.startDate.collectAsStateLifecycleAware().value,
startDate = state.task.hideUntil,
selectedDay = selectedDay,
selectedTime = selectedTime,
hasDueDate = viewModel.dueDate.collectAsStateLifecycleAware().value > 0,
hasDueDate = state.task.dueDate > 0,
printDate = {
DateUtilities.getRelativeDateTime(
context,
@ -105,7 +105,7 @@ class StartDateControlSet : TaskEditControlFragment() {
}
private fun applySelected() {
viewModel.setStartDate(vm.getSelectedValue(viewModel.dueDate.value))
viewModel.setStartDate(vm.getSelectedValue(viewModel.state.value.task.dueDate))
}
companion object {

@ -3,14 +3,31 @@ package org.tasks.compose
import android.content.res.Configuration
import androidx.annotation.StringRes
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.*
import androidx.compose.material.AlertDialog
import androidx.compose.material.Divider
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.RadioButton
import androidx.compose.material.Text
import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Autorenew
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.ExperimentalComposeUiApi
@ -30,6 +47,8 @@ import androidx.compose.ui.unit.dp
import androidx.core.content.res.ResourcesCompat
import com.google.android.material.composethemeadapter.MdcTheme
import com.todoroo.astrid.ui.ReminderControlSetViewModel.ViewState
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.coroutines.android.awaitFrame
import org.tasks.R
import org.tasks.data.Alarm
@ -467,7 +486,7 @@ fun BodyText(modifier: Modifier = Modifier, text: String) {
@Composable
fun AddAlarmDialog(
viewState: ViewState,
existingAlarms: List<Alarm>,
existingAlarms: ImmutableSet<Alarm>,
addAlarm: (Alarm) -> Unit,
addRandom: () -> Unit,
addCustom: () -> Unit,
@ -619,7 +638,7 @@ fun AddReminderDialog() =
MdcTheme {
AddAlarmDialog(
viewState = ViewState(showAddAlarm = true),
existingAlarms = emptyList(),
existingAlarms = persistentSetOf(),
addAlarm = {},
addRandom = {},
addCustom = {},

@ -2,15 +2,13 @@ package org.tasks.compose.edit
import android.content.res.Configuration
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@ -24,16 +22,13 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionStatus
import com.google.android.material.composethemeadapter.MdcTheme
import com.todoroo.astrid.ui.ReminderControlSetViewModel
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentSetOf
import org.tasks.R
import org.tasks.compose.AddAlarmDialog
import org.tasks.compose.AddReminderDialog
import org.tasks.compose.ClearButton
import org.tasks.compose.DisabledText
import org.tasks.compose.TaskEditRow
import org.tasks.compose.collectAsStateLifecycleAware
import org.tasks.compose.*
import org.tasks.data.Alarm
import org.tasks.reminders.AlarmToString
import java.util.Locale
import java.util.*
@OptIn(ExperimentalPermissionsApi::class, ExperimentalComposeUiApi::class)
@Composable
@ -41,7 +36,7 @@ fun AlarmRow(
vm: ReminderControlSetViewModel = viewModel(),
permissionStatus: PermissionStatus,
launchPermissionRequest: () -> Unit,
alarms: List<Alarm>,
alarms: ImmutableSet<Alarm>,
ringMode: Int,
locale: Locale,
addAlarm: (Alarm) -> Unit,
@ -129,7 +124,7 @@ fun AlarmRow(
@Composable
fun Alarms(
alarms: List<Alarm>,
alarms: ImmutableSet<Alarm>,
ringMode: Int,
locale: Locale,
replaceAlarm: (Alarm) -> Unit,
@ -151,7 +146,11 @@ fun Alarms(
text = stringResource(id = R.string.add_reminder),
modifier = Modifier
.padding(vertical = 12.dp)
.clickable(onClick = addAlarm)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false),
onClick = addAlarm,
)
)
Spacer(modifier = Modifier.weight(1f))
if (alarms.isNotEmpty()) {
@ -168,7 +167,11 @@ fun Alarms(
),
modifier = Modifier
.padding(vertical = 12.dp, horizontal = 16.dp)
.clickable(onClick = openRingType)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = rememberRipple(bounded = false),
onClick = openRingType
)
)
}
}
@ -204,7 +207,7 @@ private fun AlarmRow(
fun NoAlarms() {
MdcTheme {
AlarmRow(
alarms = emptyList(),
alarms = persistentSetOf(),
ringMode = 0,
locale = Locale.getDefault(),
addAlarm = {},
@ -224,7 +227,7 @@ fun NoAlarms() {
fun PermissionDenied() {
MdcTheme {
AlarmRow(
alarms = emptyList(),
alarms = persistentSetOf(),
ringMode = 0,
locale = Locale.getDefault(),
addAlarm = {},

@ -3,15 +3,31 @@ package org.tasks.compose.edit
import android.content.res.Configuration
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ContentAlpha
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.*
import androidx.compose.runtime.*
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.Cancel
import androidx.compose.material.icons.outlined.Description
import androidx.compose.material.icons.outlined.Image
import androidx.compose.material.icons.outlined.Movie
import androidx.compose.material.icons.outlined.MusicNote
import androidx.compose.material.icons.outlined.PlayCircle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
@ -36,6 +52,8 @@ import coil.request.ImageRequest
import com.google.accompanist.flowlayout.FlowRow
import com.google.android.material.composethemeadapter.MdcTheme
import com.todoroo.andlib.utility.AndroidUtilities
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentSetOf
import org.tasks.R
import org.tasks.compose.DisabledText
import org.tasks.compose.TaskEditRow
@ -46,7 +64,7 @@ private val SIZE = 128.dp
@Composable
fun AttachmentRow(
attachments: List<TaskAttachment>,
attachments: ImmutableSet<TaskAttachment>,
openAttachment: (TaskAttachment) -> Unit,
deleteAttachment: (TaskAttachment) -> Unit,
addAttachment: () -> Unit,
@ -241,7 +259,7 @@ fun BoxScope.DeleteAttachment(
fun NoAttachments() {
MdcTheme {
AttachmentRow(
attachments = emptyList(),
attachments = persistentSetOf(),
openAttachment = {},
deleteAttachment = {},
addAttachment = {},
@ -255,7 +273,7 @@ fun NoAttachments() {
fun AttachmentPreview() {
MdcTheme {
AttachmentRow(
attachments = listOf(
attachments = persistentSetOf(
TaskAttachment(
uri = "file://attachment.txt",
name = "attachment.txt",

@ -8,6 +8,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.google.android.material.composethemeadapter.MdcTheme
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentSetOf
import org.tasks.R
import org.tasks.compose.Chip
import org.tasks.compose.ChipGroup
@ -19,7 +21,7 @@ import org.tasks.themes.CustomIcons
@Composable
fun TagsRow(
tags: List<TagData>,
tags: ImmutableSet<TagData>,
colorProvider: (Int) -> Int,
onClick: () -> Unit,
onClear: (TagData) -> Unit,
@ -59,7 +61,7 @@ fun TagsRow(
fun NoTags() {
MdcTheme {
TagsRow(
tags = emptyList(),
tags = persistentSetOf(),
colorProvider = { 0 },
onClick = {},
onClear = {},
@ -73,7 +75,7 @@ fun NoTags() {
fun SingleTag() {
MdcTheme {
TagsRow(
tags = listOf(
tags = persistentSetOf(
TagData("Home").apply {
setIcon(1062)
setColor(ColorProvider.BLUE_500)
@ -91,7 +93,7 @@ fun SingleTag() {
fun BunchOfTags() {
MdcTheme {
TagsRow(
tags = listOf(
tags = persistentSetOf(
TagData("One"),
TagData("Two"),
TagData("Three"),
@ -110,7 +112,7 @@ fun BunchOfTags() {
fun TagWithReallyLongName() {
MdcTheme {
TagsRow(
tags = listOf(
tags = persistentSetOf(
TagData("This is a tag with a really really long name").apply {
setIcon(1062)
setColor(ColorProvider.BLUE_500)

@ -13,7 +13,6 @@ import org.tasks.R
import org.tasks.preferences.Preferences
import org.tasks.ui.CalendarControlSet
import org.tasks.ui.LocationControlSet
import org.tasks.ui.SubtaskControlSet
import javax.inject.Inject
class TaskEditControlSetFragmentManager @Inject constructor(
@ -46,6 +45,7 @@ class TaskEditControlSetFragmentManager @Inject constructor(
val TAG_LIST = R.string.TEA_ctrl_google_task_list
val TAG_PRIORITY = R.string.TEA_ctrl_importance_pref
val TAG_DUE_DATE = R.string.TEA_ctrl_when_pref
val TAG_SUBTASKS = R.string.TEA_ctrl_subtask_pref
private val TASK_EDIT_CONTROL_SET_FRAGMENTS = intArrayOf(
TAG_DUE_DATE,
@ -61,7 +61,7 @@ class TaskEditControlSetFragmentManager @Inject constructor(
RepeatControlSet.TAG,
TAG_CREATION,
TAG_LIST,
SubtaskControlSet.TAG
TAG_SUBTASKS,
)
}
}

@ -32,13 +32,13 @@ class CalendarControlSet : TaskEditControlFragment() {
super.onResume()
val canAccessCalendars = permissionChecker.canAccessCalendars()
viewModel.eventUri.value?.let {
viewModel.state.value.task.calendarURI?.let {
if (canAccessCalendars && !calendarEntryExists(it)) {
viewModel.eventUri.value = null
viewModel.clearCalendarUri()
}
}
if (!canAccessCalendars) {
viewModel.selectedCalendar.value = null
viewModel.setCalendar(null)
}
}
@ -46,18 +46,19 @@ class CalendarControlSet : TaskEditControlFragment() {
(parent as ComposeView).apply {
setContent {
MdcTheme {
val state = viewModel.state.collectAsStateLifecycleAware().value
CalendarRow(
eventUri = viewModel.eventUri.collectAsStateLifecycleAware().value,
selectedCalendar = viewModel.selectedCalendar.collectAsStateLifecycleAware().value?.let {
eventUri = state.task.calendarURI,
selectedCalendar = state.calendar?.let {
calendarProvider.getCalendar(it)?.name
},
onClick = {
if (viewModel.eventUri.value.isNullOrBlank()) {
if (state.task.calendarURI.isNullOrBlank()) {
CalendarPicker
.newCalendarPicker(
requireParentFragment(),
TaskEditFragment.REQUEST_CODE_PICK_CALENDAR,
viewModel.selectedCalendar.value,
state.calendar,
)
.show(
requireParentFragment().parentFragmentManager,
@ -68,8 +69,8 @@ class CalendarControlSet : TaskEditControlFragment() {
}
},
clear = {
viewModel.selectedCalendar.value = null
viewModel.eventUri.value = null
viewModel.setCalendar(null)
viewModel.clearCalendarUri()
}
)
}
@ -80,7 +81,7 @@ class CalendarControlSet : TaskEditControlFragment() {
private fun openCalendarEvent() {
val cr = activity.contentResolver
val uri = Uri.parse(viewModel.eventUri.value)
val uri = Uri.parse(viewModel.state.value.task.calendarURI)
val intent = Intent(Intent.ACTION_VIEW, uri)
try {
cr.query(
@ -90,7 +91,7 @@ class CalendarControlSet : TaskEditControlFragment() {
null).use { cursor ->
if (cursor!!.count == 0) {
activity.toast(R.string.calendar_event_not_found, duration = LENGTH_SHORT)
viewModel.eventUri.value = null
viewModel.clearCalendarUri()
} else {
cursor.moveToFirst()
intent.putExtra(CalendarContract.EXTRA_EVENT_BEGIN_TIME, cursor.getLong(0))

@ -36,11 +36,11 @@ class LocationControlSet : TaskEditControlFragment() {
@Inject lateinit var permissionChecker: PermissionChecker
private fun setLocation(location: Location?) {
viewModel.selectedLocation.value = location
viewModel.setLocation(location)
}
private fun onRowClick() {
val location = viewModel.selectedLocation.value
val location = viewModel.state.value.location
if (location == null) {
chooseLocation()
} else {
@ -66,14 +66,14 @@ class LocationControlSet : TaskEditControlFragment() {
private fun chooseLocation() {
val intent = Intent(activity, LocationPickerActivity::class.java)
viewModel.selectedLocation.value?.let {
viewModel.state.value.location?.let {
intent.putExtra(LocationPickerActivity.EXTRA_PLACE, it.place as Parcelable)
}
startActivityForResult(intent, REQUEST_LOCATION_REMINDER)
}
private fun showGeofenceOptions() {
val dialog = GeofenceDialog.newGeofenceDialog(viewModel.selectedLocation.value)
val dialog = GeofenceDialog.newGeofenceDialog(viewModel.state.value.location)
dialog.setTargetFragment(this, REQUEST_GEOFENCE_DETAILS)
dialog.show(parentFragmentManager, FRAG_TAG_LOCATION_DIALOG)
}
@ -83,11 +83,12 @@ class LocationControlSet : TaskEditControlFragment() {
(parent as ComposeView).apply {
setContent {
MdcTheme {
val state = viewModel.state.collectAsStateLifecycleAware().value
val hasPermissions =
rememberMultiplePermissionsState(permissions = backgroundPermissions())
.allPermissionsGranted
LocationRow(
location = viewModel.selectedLocation.collectAsStateLifecycleAware().value,
location = state.location,
hasPermissions = hasPermissions,
onClick = this@LocationControlSet::onRowClick,
openGeofenceOptions = {
@ -109,11 +110,11 @@ class LocationControlSet : TaskEditControlFragment() {
override fun controlId() = TAG
private fun openWebsite() {
viewModel.selectedLocation.value?.let { context?.openUri(it.url) }
viewModel.state.value.location?.let { context?.openUri(it.url) }
}
private fun call() {
viewModel.selectedLocation.value?.let {
viewModel.state.value.location?.let {
startActivity(Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + it.phone)))
}
}
@ -126,7 +127,7 @@ class LocationControlSet : TaskEditControlFragment() {
} else if (requestCode == REQUEST_LOCATION_REMINDER) {
if (resultCode == Activity.RESULT_OK) {
val place: Place = data!!.getParcelableExtra(LocationPickerActivity.EXTRA_PLACE)!!
val location = viewModel.selectedLocation.value
val location = viewModel.state.value.location
val geofence = if (location == null) {
Geofence(place.uid, preferences)
} else {
@ -143,7 +144,7 @@ class LocationControlSet : TaskEditControlFragment() {
if (resultCode == Activity.RESULT_OK) {
setLocation(Location(
data?.getParcelableExtra(GeofenceDialog.EXTRA_GEOFENCE) ?: return,
viewModel.selectedLocation.value?.place ?: return
viewModel.state.value.location?.place ?: return
))
}
} else {

@ -1,124 +0,0 @@
package org.tasks.ui
import android.app.Activity
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import com.google.android.material.composethemeadapter.MdcTheme
import com.todoroo.andlib.sql.Criterion
import com.todoroo.andlib.sql.QueryTemplate
import com.todoroo.andlib.utility.DateUtilities.now
import com.todoroo.astrid.activity.MainActivityViewModel
import com.todoroo.astrid.api.FilterImpl
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 dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.tasks.R
import org.tasks.compose.collectAsStateLifecycleAware
import org.tasks.compose.edit.SubtaskRow
import org.tasks.data.GoogleTaskDao
import org.tasks.data.TaskDao.TaskCriteria.activeAndVisible
import org.tasks.preferences.Preferences
import org.tasks.tasklist.SectionedDataSource
import org.tasks.themes.ColorProvider
import javax.inject.Inject
@AndroidEntryPoint
class SubtaskControlSet : TaskEditControlFragment() {
@Inject lateinit var activity: Activity
@Inject lateinit var taskCompleter: TaskCompleter
@Inject lateinit var googleTaskDao: GoogleTaskDao
@Inject lateinit var taskCreator: TaskCreator
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var checkBoxProvider: CheckBoxProvider
@Inject lateinit var chipProvider: ChipProvider
@Inject lateinit var colorProvider: ColorProvider
@Inject lateinit var preferences: Preferences
private lateinit var listViewModel: TaskListViewModel
private val mainViewModel: MainActivityViewModel by activityViewModels()
override fun createView(savedInstanceState: Bundle?) {
viewModel.task.takeIf { it.id > 0 }?.let {
listViewModel.setFilter(FilterImpl("subtasks", getQueryTemplate(it)))
}
}
override fun bind(parent: ViewGroup?): View =
(parent as ComposeView).apply {
listViewModel = ViewModelProvider(requireParentFragment())[TaskListViewModel::class.java]
setContent {
MdcTheme {
SubtaskRow(
originalFilter = viewModel.originalList,
filter = viewModel.selectedList.collectAsStateLifecycleAware().value,
hasParent = viewModel.hasParent,
desaturate = preferences.desaturateDarkMode,
existingSubtasks = if (viewModel.isNew) {
TaskListViewModel.TasksResults.Results(SectionedDataSource())
} else {
listViewModel.state.collectAsStateLifecycleAware().value.tasks
},
newSubtasks = viewModel.newSubtasks.collectAsStateLifecycleAware().value,
openSubtask = this@SubtaskControlSet::openSubtask,
completeExistingSubtask = this@SubtaskControlSet::complete,
toggleSubtask = this@SubtaskControlSet::toggleSubtask,
addSubtask = this@SubtaskControlSet::addSubtask,
completeNewSubtask = {
viewModel.newSubtasks.value =
ArrayList(viewModel.newSubtasks.value).apply {
val modified = it.copy(
completionDate = if (it.isCompleted) 0 else now()
)
set(indexOf(it), modified)
}
},
deleteSubtask = {
viewModel.newSubtasks.value =
ArrayList(viewModel.newSubtasks.value).apply {
remove(it)
}
}
)
}
}
}
override fun controlId() = TAG
private fun addSubtask() = lifecycleScope.launch {
val task = taskCreator.createWithValues("")
viewModel.newSubtasks.value = viewModel.newSubtasks.value.plus(task)
}
private fun openSubtask(task: Task) = lifecycleScope.launch {
mainViewModel.setTask(task)
}
private fun toggleSubtask(taskId: Long, collapsed: Boolean) = lifecycleScope.launch {
taskDao.setCollapsed(taskId, collapsed)
}
private fun complete(task: Task, completed: Boolean) = lifecycleScope.launch {
taskCompleter.setComplete(task, completed)
}
companion object {
val TAG = R.string.TEA_ctrl_subtask_pref
private fun getQueryTemplate(task: Task): String = QueryTemplate()
.where(
Criterion.and(
activeAndVisible(),
Task.PARENT.eq(task.id)
)
)
.toString()
}
}

@ -20,16 +20,27 @@ import com.todoroo.astrid.data.Task.Companion.NOTIFY_MODE_FIVE
import com.todoroo.astrid.data.Task.Companion.NOTIFY_MODE_NONSTOP
import com.todoroo.astrid.data.Task.Companion.createDueDate
import com.todoroo.astrid.data.Task.Companion.hasDueTime
import com.todoroo.astrid.data.Task.RepeatFrom.Companion.COMPLETION_DATE
import com.todoroo.astrid.data.Task.RepeatFrom.Companion.DUE_DATE
import com.todoroo.astrid.gcal.GCalHelper
import com.todoroo.astrid.service.TaskCompleter
import com.todoroo.astrid.service.TaskCreator
import com.todoroo.astrid.service.TaskCreator.Companion.getDefaultAlarms
import com.todoroo.astrid.service.TaskDeleter
import com.todoroo.astrid.service.TaskMover
import com.todoroo.astrid.timers.TimerPlugin
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.PersistentSet
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toPersistentSet
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@ -60,6 +71,7 @@ import org.tasks.files.FileHelper
import org.tasks.location.GeofenceApi
import org.tasks.preferences.PermissionChecker
import org.tasks.preferences.Preferences
import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils.currentTimeMillis
import org.tasks.time.DateTimeUtils.startOfDay
import timber.log.Timber
@ -67,199 +79,130 @@ import javax.inject.Inject
@HiltViewModel
class TaskEditViewModel @Inject constructor(
@ApplicationContext private val context: Context,
savedStateHandle: SavedStateHandle,
private val taskDao: TaskDao,
private val taskDeleter: TaskDeleter,
private val timerPlugin: TimerPlugin,
private val permissionChecker: PermissionChecker,
private val calendarEventProvider: CalendarEventProvider,
private val gCalHelper: GCalHelper,
private val taskMover: TaskMover,
private val locationDao: LocationDao,
private val geofenceApi: GeofenceApi,
private val tagDao: TagDao,
private val tagDataDao: TagDataDao,
private val preferences: Preferences,
private val googleTaskDao: GoogleTaskDao,
private val caldavDao: CaldavDao,
private val taskCompleter: TaskCompleter,
private val alarmService: AlarmService,
private val taskListEvents: TaskListEventBus,
private val mainActivityEvents: MainActivityEventBus,
private val firebase: Firebase? = null,
private val userActivityDao: UserActivityDao,
private val alarmDao: AlarmDao,
private val taskAttachmentDao: TaskAttachmentDao,
@ApplicationContext private val applicationContext: Context,
savedStateHandle: SavedStateHandle,
private val taskDao: TaskDao,
private val taskDeleter: TaskDeleter,
private val timerPlugin: TimerPlugin,
private val permissionChecker: PermissionChecker,
private val calendarEventProvider: CalendarEventProvider,
private val gCalHelper: GCalHelper,
private val taskMover: TaskMover,
private val locationDao: LocationDao,
private val geofenceApi: GeofenceApi,
private val tagDao: TagDao,
private val tagDataDao: TagDataDao,
private val preferences: Preferences,
private val googleTaskDao: GoogleTaskDao,
private val caldavDao: CaldavDao,
private val taskCompleter: TaskCompleter,
private val alarmService: AlarmService,
private val taskListEvents: TaskListEventBus,
private val mainActivityEvents: MainActivityEventBus,
private val firebase: Firebase? = null,
private val userActivityDao: UserActivityDao,
private val alarmDao: AlarmDao,
private val taskAttachmentDao: TaskAttachmentDao,
private val taskCreator: TaskCreator,
) : ViewModel() {
private val resources = context.resources
private var cleared = false
val task: Task = savedStateHandle[TaskEditFragment.EXTRA_TASK]!!
val isNew = task.isNew
var creationDate: Long = task.creationDate
var modificationDate: Long = task.modificationDate
var completionDate: Long = task.completionDate
var title: String? = task.title
var completed: Boolean = task.isCompleted
var priority = MutableStateFlow(task.priority)
var description: String? = task.notes.stripCarriageReturns()
val recurrence = MutableStateFlow(task.recurrence)
val repeatAfterCompletion = MutableStateFlow(task.repeatAfterCompletion())
var eventUri = MutableStateFlow(task.calendarURI)
val timerStarted = MutableStateFlow(task.timerStart)
val estimatedSeconds = MutableStateFlow(task.estimatedSeconds)
val elapsedSeconds = MutableStateFlow(task.elapsedSeconds)
var newSubtasks = MutableStateFlow(emptyList<Task>())
val hasParent: Boolean
get() = task.parent > 0
val dueDate = MutableStateFlow(task.dueDate)
fun setDueDate(value: Long) {
dueDate.value = when {
value == 0L -> 0
hasDueTime(value) -> createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, value)
else -> createDueDate(Task.URGENCY_SPECIFIC_DAY, value)
}
}
val startDate = MutableStateFlow(task.hideUntil)
fun setStartDate(value: Long) {
startDate.value = when {
value == 0L -> 0
hasDueTime(value) ->
value.toDateTime().withSecondOfMinute(1).withMillisOfSecond(0).millis
else -> value.startOfDay()
}
}
private var originalCalendar: String? = if (isNew && permissionChecker.canAccessCalendars()) {
preferences.defaultCalendar
} else {
null
}
var selectedCalendar = MutableStateFlow(originalCalendar)
val originalList: Filter = savedStateHandle[TaskEditFragment.EXTRA_LIST]!!
var selectedList = MutableStateFlow(originalList)
data class State(
val task: Task,
val filter: Filter,
val calendar: String? = null,
val location: Location? = null,
val tags: PersistentSet<TagData> = persistentSetOf(),
val attachments: PersistentSet<TaskAttachment> = persistentSetOf(),
val alarms: PersistentSet<Alarm> = persistentSetOf(),
val newSubtasks: PersistentList<Task> = persistentListOf(),
)
private val resources = applicationContext.resources
private var cleared = false
private var originalLocation: Location? = savedStateHandle[TaskEditFragment.EXTRA_LOCATION]
var selectedLocation = MutableStateFlow(originalLocation)
private var originalState: State
private val _state: MutableStateFlow<State>
val state: StateFlow<State>
private val originalTags: List<TagData> =
savedStateHandle.get<ArrayList<TagData>>(TaskEditFragment.EXTRA_TAGS) ?: emptyList()
val selectedTags = MutableStateFlow(ArrayList(originalTags))
val originalTask: Task
get() = originalState.task
private lateinit var originalAttachments: List<TaskAttachment>
val selectedAttachments = MutableStateFlow(emptyList<TaskAttachment>())
val originalFilter: Filter
get() = originalState.filter
private val originalAlarms: List<Alarm> = if (isNew) {
ArrayList<Alarm>().apply {
if (task.isNotifyAtStart) {
add(Alarm.whenStarted(0))
}
if (task.isNotifyAtDeadline) {
add(Alarm.whenDue(0))
}
if (task.isNotifyAfterDeadline) {
add(Alarm.whenOverdue(0))
}
if (task.randomReminder > 0) {
add(Alarm(0, task.randomReminder, Alarm.TYPE_RANDOM))
}
init {
val task: Task = savedStateHandle[TaskEditFragment.EXTRA_TASK]
?: throw IllegalStateException("No task")
val tags: List<TagData> = savedStateHandle[TaskEditFragment.EXTRA_TAGS] ?: emptyList()
originalState = State(
task = task.copy(
notes = task.notes.stripCarriageReturns(),
calendarURI = task.calendarURI?.takeIf { it.isNotBlank() },
),
filter = savedStateHandle[TaskEditFragment.EXTRA_LIST]
?: throw IllegalStateException("No list"),
tags = tags.toPersistentSet(),
location = savedStateHandle[TaskEditFragment.EXTRA_LOCATION],
calendar = if (task.isNew && permissionChecker.canAccessCalendars()) {
preferences.defaultCalendar
} else {
null
},
alarms = if (task.isNew) {
ArrayList<Alarm>().apply {
if (task.isNotifyAtStart) {
add(Alarm.whenStarted(0))
}
if (task.isNotifyAtDeadline) {
add(Alarm.whenDue(0))
}
if (task.isNotifyAfterDeadline) {
add(Alarm.whenOverdue(0))
}
if (task.randomReminder > 0) {
add(Alarm(0, task.randomReminder, Alarm.TYPE_RANDOM))
}
}
} else {
savedStateHandle[TaskEditFragment.EXTRA_ALARMS] ?: emptyList()
}.toPersistentSet()
)
_state = MutableStateFlow(originalState)
state = _state.asStateFlow()
viewModelScope.launch(Dispatchers.Default) {
val attachments = taskAttachmentDao.getAttachments(task.id).toPersistentSet()
originalState = originalState.copy(attachments = attachments)
_state.update { it.copy(attachments = attachments) }
}
} else {
savedStateHandle[TaskEditFragment.EXTRA_ALARMS]!!
}
var selectedAlarms = MutableStateFlow(originalAlarms)
@Deprecated("delete me")
val isNew = originalState.task.isNew
var ringNonstop: Boolean = task.isNotifyModeNonstop
set(value) {
field = value
if (value) {
ringFiveTimes = false
}
}
var ringFiveTimes:Boolean = task.isNotifyModeFive
set(value) {
field = value
if (value) {
ringNonstop = false
}
}
val isReadOnly = task.readOnly
val isReadOnly = originalState.task.readOnly
val isWritable = !isReadOnly
fun hasChanges(): Boolean =
(task.title != title || (isNew && title?.isNotBlank() == true)) ||
task.isCompleted != completed ||
task.dueDate != dueDate.value ||
task.priority != priority.value ||
if (task.notes.isNullOrBlank()) {
!description.isNullOrBlank()
} else {
task.notes != description
} ||
task.hideUntil != startDate.value ||
if (task.recurrence.isNullOrBlank()) {
!recurrence.value.isNullOrBlank()
} else {
task.recurrence != recurrence.value
} ||
task.repeatAfterCompletion() != repeatAfterCompletion.value ||
originalCalendar != selectedCalendar.value ||
if (task.calendarURI.isNullOrBlank()) {
!eventUri.value.isNullOrBlank()
} else {
task.calendarURI != eventUri.value
} ||
task.elapsedSeconds != elapsedSeconds.value ||
task.estimatedSeconds != estimatedSeconds.value ||
originalList != selectedList.value ||
originalLocation != selectedLocation.value ||
originalTags.toHashSet() != selectedTags.value.toHashSet() ||
(::originalAttachments.isInitialized &&
originalAttachments.toHashSet() != selectedAttachments.value.toHashSet()) ||
newSubtasks.value.isNotEmpty() ||
getRingFlags() != when {
task.isNotifyModeFive -> NOTIFY_MODE_FIVE
task.isNotifyModeNonstop -> NOTIFY_MODE_NONSTOP
else -> 0
} ||
originalAlarms.toHashSet() != selectedAlarms.value.toHashSet()
fun hasChanges() = originalState != _state.value
@MainThread
suspend fun save(remove: Boolean = true): Boolean = withContext(NonCancellable) {
suspend fun save(remove: Boolean = true): Task? = withContext(NonCancellable) {
if (cleared) {
return@withContext false
return@withContext null
}
if (!hasChanges() || isReadOnly) {
discard(remove)
return@withContext false
return@withContext null
}
clear(remove)
task.title = if (title.isNullOrBlank()) resources.getString(R.string.no_title) else title
task.dueDate = dueDate.value
task.priority = priority.value
task.notes = description
task.hideUntil = startDate.value
task.recurrence = recurrence.value
task.repeatFrom = if (repeatAfterCompletion.value) {
Task.RepeatFrom.COMPLETION_DATE
} else {
Task.RepeatFrom.DUE_DATE
}
task.elapsedSeconds = elapsedSeconds.value
task.estimatedSeconds = estimatedSeconds.value
task.ringFlags = getRingFlags()
val originalTask = originalState.task
val state = _state.value
var task = state.task
if (task.title.isNullOrBlank()) {
task = task.copy(title = resources.getString(R.string.no_title))
}
applyCalendarChanges()
@ -267,14 +210,14 @@ class TaskEditViewModel @Inject constructor(
taskDao.createNew(task)
}
if ((isNew && selectedLocation.value != null) || originalLocation != selectedLocation.value) {
originalLocation?.let { location ->
if ((isNew && state.location != null) || originalState.location != state.location) {
originalState.location?.let { location ->
if (location.geofence.id > 0) {
locationDao.delete(location.geofence)
geofenceApi.update(location.place)
}
}
selectedLocation.value?.let { location ->
state.location?.let { location ->
val place = location.place
locationDao.insert(
location.geofence.copy(
@ -288,26 +231,34 @@ class TaskEditViewModel @Inject constructor(
task.modificationDate = currentTimeMillis()
}
if ((isNew && selectedTags.value.isNotEmpty()) || originalTags.toHashSet() != selectedTags.value.toHashSet()) {
tagDao.applyTags(task, tagDataDao, selectedTags.value)
if ((isNew && state.tags.isNotEmpty()) || originalState.tags.toHashSet() != state.tags.toHashSet()) {
tagDao.applyTags(task, tagDataDao, state.tags.toList())
task.modificationDate = currentTimeMillis()
}
if (!task.hasStartDate()) {
selectedAlarms.value = selectedAlarms.value.filterNot { a -> a.type == TYPE_REL_START }
_state.update {
it.copy(
alarms = it.alarms.removeAll { a -> a.type == TYPE_REL_START }
)
}
}
if (!task.hasDueDate()) {
selectedAlarms.value = selectedAlarms.value.filterNot { a -> a.type == TYPE_REL_END }
_state.update {
it.copy(
alarms = it.alarms.removeAll { a -> a.type == TYPE_REL_END }
)
}
}
taskDao.save(task, null)
if (isNew || originalList != selectedList.value) {
if (isNew || originalState.filter != state.filter) {
task.parent = 0
taskMover.move(listOf(task.id), selectedList.value)
taskMover.move(listOf(task.id), state.filter)
}
for (subtask in newSubtasks.value) {
for (subtask in state.newSubtasks) {
if (Strings.isNullOrEmpty(subtask.title)) {
continue
}
@ -317,7 +268,7 @@ class TaskEditViewModel @Inject constructor(
taskDao.createNew(subtask)
alarmDao.insert(subtask.getDefaultAlarms())
firebase?.addTask("subtasks")
when (val filter = selectedList.value) {
when (val filter = state.filter) {
is GtasksFilter -> {
val googleTask = CaldavTask(
task = subtask.id,
@ -354,24 +305,23 @@ class TaskEditViewModel @Inject constructor(
}
if (
selectedAlarms.value.toHashSet() != originalAlarms.toHashSet() ||
(isNew && selectedAlarms.value.isNotEmpty())
originalState.alarms.toHashSet() != state.alarms.toHashSet() ||
(isNew && state.alarms.isNotEmpty())
) {
alarmService.synchronizeAlarms(task.id, selectedAlarms.value.toMutableSet())
alarmService.synchronizeAlarms(task.id, state.alarms.toMutableSet())
task.putTransitory(SyncFlags.FORCE_CALDAV_SYNC, true)
task.modificationDate = now()
}
if (
this@TaskEditViewModel::originalAttachments.isInitialized &&
selectedAttachments.value.toHashSet() != originalAttachments.toHashSet()
) {
originalAttachments
.minus(selectedAttachments.value.toSet())
if (state.attachments.toHashSet() != originalState.attachments.toHashSet()) {
originalState
.attachments
.minus(state.attachments.toSet())
.map { it.remoteId }
.let { taskAttachmentDao.delete(task.id, it) }
selectedAttachments.value
.minus(originalAttachments.toSet())
state
.attachments
.minus(originalState.attachments.toSet())
.map {
Attachment(
task = task.id,
@ -382,8 +332,8 @@ class TaskEditViewModel @Inject constructor(
.let { taskAttachmentDao.insert(it) }
}
if (task.isCompleted != completed) {
taskCompleter.setComplete(task, completed)
if (task.isCompleted != originalTask.isCompleted) {
taskCompleter.setComplete(task, task.isCompleted)
}
if (isNew) {
@ -393,44 +343,45 @@ class TaskEditViewModel @Inject constructor(
taskListEvents.emit(TaskListEvent.CalendarEventCreated(model.title, it))
}
}
true
task
}
private suspend fun applyCalendarChanges() {
if (!permissionChecker.canAccessCalendars()) {
return
}
if (eventUri.value == null) {
val task = state.value.task
if (task.calendarURI == null) {
calendarEventProvider.deleteEvent(task)
}
if (!task.hasDueDate()) {
return
}
selectedCalendar.value?.let {
_state.value.calendar?.let { selectedCalendar ->
try {
task.calendarURI = gCalHelper.createTaskEvent(task, it)?.toString()
_state.update {
it.copy(
task = it.task.copy(
calendarURI = gCalHelper.createTaskEvent(it.task, selectedCalendar)?.toString()
)
)
}
} catch (e: Exception) {
Timber.e(e)
}
}
}
private fun getRingFlags() = when {
ringNonstop -> NOTIFY_MODE_NONSTOP
ringFiveTimes -> NOTIFY_MODE_FIVE
else -> 0
}
suspend fun delete() {
taskDeleter.markDeleted(task)
taskDeleter.markDeleted(_state.value.task.id)
discard()
}
suspend fun discard(remove: Boolean = true) {
if (isNew) {
timerPlugin.stopTimer(task)
originalAttachments.plus(selectedAttachments.value).toSet().takeIf { it.isNotEmpty() }
?.onEach { FileHelper.delete(context, it.uri.toUri()) }
timerPlugin.stopTimer(_state.value.task)
originalState.attachments.plus(state.value.attachments).toSet().takeIf { it.isNotEmpty() }
?.onEach { FileHelper.delete(applicationContext, it.uri.toUri()) }
?.let { taskAttachmentDao.delete(it.toList()) }
}
clear(remove)
@ -455,14 +406,87 @@ class TaskEditViewModel @Inject constructor(
}
}
fun removeAlarm(alarm: Alarm) {
selectedAlarms.update { it.minus(alarm) }
}
fun setTitle(title: String) =
_state.update { it.copy(task = it.task.copy(title = title)) }
fun setCompleted(completed: Boolean) =
_state.update { it.copy(task = it.task.copy(completionDate = if (completed) now() else 0)) }
fun setPriority(@Task.Priority priority: Int) =
_state.update { it.copy(task = it.task.copy(priority = priority)) }
fun setDescription(description: String?) =
_state.update { it.copy(task = it.task.copy(notes = description)) }
fun setRecurrence(recurrence: String?) =
_state.update {
it.copy(
task = it.task.copy(
recurrence = recurrence,
dueDate = if (it.task.dueDate == 0L && recurrence?.isNotBlank() == true) {
DateTime().startOfDay().millis
} else {
it.task.dueDate
}
)
)
}
fun setRepeatAfterCompletion(repeatAfterCompletion: Boolean) =
_state.update {
it.copy(
task = it.task.copy(
repeatFrom = if (repeatAfterCompletion) COMPLETION_DATE else DUE_DATE
)
)
}
fun clearCalendarUri() =
_state.update { it.copy(task = it.task.copy(calendarURI = null)) }
fun setCalendar(calendar: String?) =
_state.update { it.copy(calendar = calendar) }
fun setDueDate(value: Long) =
_state.update {
it.copy(
task = it.task.copy(
dueDate = when {
value == 0L -> 0
hasDueTime(value) -> createDueDate(Task.URGENCY_SPECIFIC_DAY_TIME, value)
else -> createDueDate(Task.URGENCY_SPECIFIC_DAY, value)
}
)
)
}
fun setStartDate(value: Long) =
_state.update {
it.copy(
task = it.task.copy(
hideUntil = when {
value == 0L -> 0
hasDueTime(value) ->
value.toDateTime().withSecondOfMinute(1).withMillisOfSecond(0).millis
else -> value.startOfDay()
}
)
)
}
fun removeAlarm(alarm: Alarm) =
_state.update {
it.copy(
alarms = it.alarms.remove(alarm)
)
}
fun addAlarm(alarm: Alarm) {
with (selectedAlarms) {
if (value.none { it.same(alarm) }) {
value = value.plus(alarm)
if (state.value.alarms.none { it.same(alarm) }) {
_state.update {
it.copy(
alarms = it.alarms.add(alarm)
)
}
}
}
@ -470,11 +494,11 @@ class TaskEditViewModel @Inject constructor(
fun addComment(message: String?, picture: Uri?) {
val userActivity = UserActivity()
if (picture != null) {
val output = FileHelper.copyToUri(context, preferences.attachmentsDirectory!!, picture)
val output = FileHelper.copyToUri(applicationContext, preferences.attachmentsDirectory!!, picture)
userActivity.setPicture(output)
}
userActivity.message = message
userActivity.targetId = task.uuid
userActivity.targetId = originalState.task.uuid
userActivity.created = now()
viewModelScope.launch {
withContext(NonCancellable) {
@ -483,13 +507,77 @@ class TaskEditViewModel @Inject constructor(
}
}
init {
viewModelScope.launch {
taskAttachmentDao.getAttachments(task.id).let { attachments ->
selectedAttachments.update { attachments }
originalAttachments = attachments
}
fun setFilter(filter: Filter) =
_state.update { it.copy(filter = filter) }
fun setLocation(location: Location?) =
_state.update { it.copy(location = location) }
fun setTags(tags: List<TagData>) =
_state.update { it.copy(tags = tags.toPersistentSet()) }
fun setTimer(estimated: Int, elapsed: Int) =
_state.update {
it.copy(
task = it.task.copy(
estimatedSeconds = estimated,
elapsedSeconds = elapsed,
)
)
}
fun setTimerStarted(timerStart: Long) =
_state.update { it.copy(task = it.task.copy(timerStart = timerStart) ) }
fun setElapsed(elapsedSeconds: Int) =
_state.update { it.copy(task = it.task.copy(elapsedSeconds = elapsedSeconds)) }
fun removeNewSubtask(task: Task) =
_state.update { it.copy(newSubtasks = it.newSubtasks.remove(task)) }
fun addSubtask() = viewModelScope.launch {
_state.update { it.copy(newSubtasks = it.newSubtasks.add(taskCreator.createWithValues(""))) }
}
fun toggleNewSubtaskCompleted(task: Task) =
_state.update {
it.copy(
newSubtasks = it.newSubtasks.set(
it.newSubtasks.indexOf(task),
task.copy(completionDate = if (task.isCompleted) 0 else now())
)
)
}
fun deleteAttachment(attachment: TaskAttachment) =
_state.update { it.copy(attachments = it.attachments.remove(attachment)) }
fun addAttachment(attachment: TaskAttachment) =
_state.update { it.copy(attachments = it.attachments.add(attachment)) }
fun removeTag(tag: TagData) =
_state.update { it.copy(tags = it.tags.remove(tag)) }
fun setRingMode(ringMode: Int) {
_state.update {
it.copy(
task = it.task.copy(
ringFlags = when (ringMode) {
1 -> NOTIFY_MODE_FIVE
2 -> NOTIFY_MODE_NONSTOP
else -> 0
}
)
)
}
}
fun setCompleted(task: Task, completed: Boolean) = viewModelScope.launch {
taskCompleter.setComplete(task, completed)
}
fun toggleSubtask(taskId: Long, collapsed: Boolean) = viewModelScope.launch {
taskDao.setCollapsed(taskId, collapsed)
}
companion object {

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/task_edit_subtasks"
android:name="org.tasks.ui.SubtaskControlSet"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
Loading…
Cancel
Save