Added TaskEditScreen - WIP

pull/3221/head
Alex Baker 1 year ago
parent 833eb81114
commit 95ae988fd7

@ -1,11 +1,11 @@
package org.tasks.ui.editviewmodel
import com.natpryce.makeiteasy.MakeItEasy
import org.tasks.data.entity.Task
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import org.junit.Assert
import org.junit.Test
import org.tasks.data.entity.Task
import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker
@ -16,7 +16,7 @@ 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())
}
@ -25,7 +25,7 @@ class PriorityTests : BaseTaskEditViewModelTest() {
fun applyPriorityChange() {
val task = TaskMaker.newTask(MakeItEasy.with(TaskMaker.PRIORITY, Task.Priority.HIGH))
setup(task)
viewModel.priority.value = Task.Priority.MEDIUM
viewModel.setPriority(Task.Priority.MEDIUM)
save()
@ -36,8 +36,8 @@ class PriorityTests : BaseTaskEditViewModelTest() {
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())
}

@ -39,7 +39,7 @@ class ReminderTests : BaseTaskEditViewModelTest() {
assertEquals(
listOf(Alarm(type = Alarm.TYPE_REL_START)),
viewModel.selectedAlarms.value
viewModel.viewState.value.alarms
)
}
@ -56,7 +56,7 @@ class ReminderTests : BaseTaskEditViewModelTest() {
assertEquals(
listOf(Alarm(type = Alarm.TYPE_REL_END)),
viewModel.selectedAlarms.value
viewModel.viewState.value.alarms
)
}
@ -73,7 +73,7 @@ class ReminderTests : BaseTaskEditViewModelTest() {
assertEquals(
listOf(whenOverdue(0)),
viewModel.selectedAlarms.value
viewModel.viewState.value.alarms
)
}

@ -1,12 +1,12 @@
package org.tasks.ui.editviewmodel
import org.tasks.data.entity.Task
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.assertTrue
import org.junit.Test
import org.tasks.data.entity.Task
import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker.newTask
@ -33,7 +33,7 @@ class TaskEditViewModelTest : BaseTaskEditViewModelTest() {
fun dontSaveTaskTwice() = runBlocking {
setup(newTask())
viewModel.priority.value = Task.Priority.HIGH
viewModel.setPriority(Task.Priority.HIGH)
assertTrue(save())

@ -1,13 +1,13 @@
package org.tasks.ui.editviewmodel
import com.natpryce.makeiteasy.MakeItEasy.with
import org.tasks.data.entity.Task.Priority.Companion.HIGH
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.data.entity.Task.Priority.Companion.HIGH
import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker
import org.tasks.makers.TaskMaker.newTask
@ -19,7 +19,7 @@ class TitleTests : BaseTaskEditViewModelTest() {
fun changeTitleCausesChange() {
setup(newTask())
viewModel.title = "Test"
viewModel.setTitle("Test")
assertTrue(viewModel.hasChanges())
}
@ -29,7 +29,7 @@ class TitleTests : BaseTaskEditViewModelTest() {
val task = newTask()
setup(task)
viewModel.priority.value = HIGH
viewModel.setPriority(HIGH)
save()

@ -8,31 +8,13 @@ import android.view.View
import android.view.ViewGroup
import androidx.activity.compose.BackHandler
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material.icons.outlined.Clear
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.Save
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.viewinterop.AndroidViewBinding
import androidx.core.os.BundleCompat
import androidx.fragment.app.Fragment
@ -40,7 +22,6 @@ import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import com.todoroo.andlib.utility.AndroidUtilities.atLeastOreoMR1
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.files.FilesControlSet
import com.todoroo.astrid.repeats.RepeatControlSet
import com.todoroo.astrid.tags.TagsControlSet
@ -50,29 +31,25 @@ 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 kotlinx.coroutines.runBlocking
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.analytics.Firebase
import org.tasks.calendars.CalendarPicker
import org.tasks.compose.BeastModeBanner
import org.tasks.compose.FilterSelectionActivity.Companion.launch
import org.tasks.compose.FilterSelectionActivity.Companion.registerForListPickerResult
import org.tasks.compose.edit.CommentsRow
import org.tasks.compose.edit.DescriptionRow
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.TaskEditScreen
import org.tasks.compose.edit.TitleRow
import org.tasks.data.Location
import org.tasks.data.dao.UserActivityDao
import org.tasks.data.entity.Alarm
import org.tasks.data.entity.TagData
import org.tasks.data.entity.Task
import org.tasks.databinding.TaskEditCalendarBinding
import org.tasks.databinding.TaskEditCommentBarBinding
import org.tasks.databinding.TaskEditFilesBinding
import org.tasks.databinding.TaskEditLocationBinding
import org.tasks.databinding.TaskEditRemindersBinding
@ -87,14 +64,7 @@ import org.tasks.dialogs.DialogBuilder
import org.tasks.dialogs.Linkify
import org.tasks.extensions.Context.is24HourFormat
import org.tasks.extensions.hideKeyboard
import org.tasks.files.FileHelper
import org.tasks.filters.Filter
import org.tasks.fragments.TaskEditControlSetFragmentManager
import org.tasks.fragments.TaskEditControlSetFragmentManager.Companion.TAG_CREATION
import org.tasks.fragments.TaskEditControlSetFragmentManager.Companion.TAG_DESCRIPTION
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.kmp.org.tasks.time.DateStyle
import org.tasks.kmp.org.tasks.time.getRelativeDateTime
import org.tasks.markdown.MarkdownProvider
@ -110,20 +80,21 @@ 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.TaskEditViewModel.Companion.TAG_CREATION
import org.tasks.ui.TaskEditViewModel.Companion.TAG_DESCRIPTION
import org.tasks.ui.TaskEditViewModel.Companion.TAG_DUE_DATE
import org.tasks.ui.TaskEditViewModel.Companion.TAG_LIST
import org.tasks.ui.TaskEditViewModel.Companion.TAG_PRIORITY
import org.tasks.ui.TaskEditViewModel.Companion.TAG_TITLE
import java.util.Locale
import javax.inject.Inject
@AndroidEntryPoint
class TaskEditFragment : Fragment() {
@Inject lateinit var taskDao: TaskDao
@Inject lateinit var userActivityDao: UserActivityDao
@Inject lateinit var notificationManager: NotificationManager
@Inject lateinit var dialogBuilder: DialogBuilder
@Inject lateinit var context: Activity
@Inject lateinit var taskEditControlSetFragmentManager: TaskEditControlSetFragmentManager
@Inject lateinit var preferences: Preferences
@Inject lateinit var firebase: Firebase
@Inject lateinit var linkify: Linkify
@Inject lateinit var markdownProvider: MarkdownProvider
@Inject lateinit var taskEditEventBus: TaskEditEventBus
@ -133,145 +104,121 @@ class TaskEditFragment : Fragment() {
@Inject lateinit var theme: Theme
private val editViewModel: TaskEditViewModel by viewModels()
private var showKeyboard = false
private val beastMode =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
activity?.recreate()
}
private val listPickerLauncher = registerForListPickerResult { filter ->
editViewModel.selectedList.update { filter }
editViewModel.setList(filter)
}
val task: Task?
get() = BundleCompat.getParcelable(requireArguments(), EXTRA_TASK, Task::class.java)
@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterial3Api::class)
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
if (atLeastOreoMR1()) {
activity?.setShowWhenLocked(preferences.showEditScreenWithoutUnlock)
}
val model = editViewModel.task
if (!model.isNew) {
lifecycleScope.launch {
notificationManager.cancel(model.id)
}
}
if (savedInstanceState == null) {
showKeyboard = model.isNew && isNullOrEmpty(model.title)
}
val backButtonSavesTask = preferences.backButtonSavesTask()
val view = ComposeView(context).apply {
val view = ComposeView(requireActivity()).apply {
setContent {
BackHandler {
if (backButtonSavesTask) {
lifecycleScope.launch {
save()
TasksTheme(theme = theme.themeBase.index,) {
val viewState = editViewModel.viewState.collectAsStateWithLifecycle().value
BackHandler {
if (viewState.backButtonSavesTask) {
lifecycleScope.launch {
save()
}
} else {
discardButtonClick()
}
} else {
discardButtonClick()
}
}
TasksTheme(theme = theme.themeBase.index,) {
Scaffold(
topBar = {
TopAppBar(
navigationIcon = {
if (editViewModel.isReadOnly) {
IconButton(onClick = { activity?.onBackPressed() }) {
Icon(
imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
contentDescription = stringResource(R.string.back)
)
}
} else {
IconButton(onClick = { lifecycleScope.launch { save() } }) {
Icon(
imageVector = Icons.Outlined.Save,
contentDescription = stringResource(R.string.save)
)
}
}
},
title = {},
actions = {
if (!editViewModel.isWritable) {
return@TopAppBar
}
if (!editViewModel.isNew) {
IconButton(onClick = { deleteButtonClick() }) {
Icon(
imageVector = Icons.Outlined.Delete,
contentDescription = stringResource(R.string.delete_task),
)
}
}
IconButton(onClick = { discardButtonClick() }) {
Icon(
imageVector = Icons.Outlined.Clear,
contentDescription = stringResource(R.string.menu_discard_changes),
)
}
},
)
LaunchedEffect(viewState.isNew) {
if (!viewState.isNew) {
notificationManager.cancel(viewState.task.id)
}
}
TaskEditScreen(
viewState = viewState,
comments = userActivityDao
.watchComments(viewState.task.uuid)
.collectAsStateWithLifecycle(emptyList())
.value,
save = { lifecycleScope.launch { save() } },
discard = { discardButtonClick() },
onBackPressed = { activity?.onBackPressed() },
delete = { deleteButtonClick() },
openBeastModeSettings = {
editViewModel.hideBeastModeHint(click = true)
beastMode.launch(Intent(context, BeastModePreferences::class.java))
},
bottomBar = {
if (preferences.getBoolean(R.string.p_show_task_edit_comments, false)) {
AndroidViewBinding(TaskEditCommentBarBinding::inflate)
dismissBeastMode = { editViewModel.hideBeastModeHint(click = false) },
deleteComment = {
lifecycleScope.launch {
userActivityDao.delete(it)
}
},
) { paddingValues ->
Column(
modifier = Modifier
.gesturesDisabled(editViewModel.isReadOnly)
.padding(paddingValues)
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
TitleRow(requestFocus = showKeyboard)
HorizontalDivider()
taskEditControlSetFragmentManager.displayOrder.forEachIndexed { index, tag ->
if (index < taskEditControlSetFragmentManager.visibleSize) {
// TODO: remove ui-viewbinding library when these are all migrated
when (taskEditControlSetFragmentManager.controlSetFragments[tag]) {
TAG_DUE_DATE -> DueDateRow()
TAG_PRIORITY -> PriorityRow()
TAG_DESCRIPTION -> DescriptionRow()
TAG_LIST -> ListRow()
TAG_CREATION -> CreationRow()
CalendarControlSet.TAG -> AndroidViewBinding(TaskEditCalendarBinding::inflate)
StartDateControlSet.TAG -> AndroidViewBinding(
TaskEditStartDateBinding::inflate
)
ReminderControlSet.TAG -> AndroidViewBinding(
TaskEditRemindersBinding::inflate
) { tag ->
// TODO: remove ui-viewbinding library when these are all migrated
when (tag) {
TAG_TITLE ->
TitleRow(
viewState = viewState,
requestFocus = viewState.showKeyboard,
)
TAG_DUE_DATE -> DueDateRow()
TAG_PRIORITY ->
PriorityRow(
priority = viewState.task.priority,
onChangePriority = { editViewModel.setPriority(it) },
)
TAG_DESCRIPTION ->
DescriptionRow(
text = viewState.task.notes,
onChanged = { text -> editViewModel.setDescription(text.toString().trim { it <= ' ' }) },
linkify = if (viewState.linkify) linkify else null,
markdownProvider = markdownProvider,
)
TAG_LIST ->
ListRow(
list = viewState.list,
colorProvider = { chipProvider.getColor(it) },
onClick = {
listPickerLauncher.launch(
context = context,
selectedFilter = viewState.list,
listsOnly = true
)
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")
}
HorizontalDivider()
}
}
if (preferences.getBoolean(R.string.p_show_task_edit_comments, false)) {
Comments()
}
val showBeastModeHint = editViewModel.showBeastModeHint.collectAsStateWithLifecycle().value
val context = LocalContext.current
BeastModeBanner(
showBeastModeHint,
showSettings = {
editViewModel.hideBeastModeHint(click = true)
beastMode.launch(Intent(context, BeastModePreferences::class.java))
},
dismiss = {
editViewModel.hideBeastModeHint(click = false)
}
)
TAG_CREATION ->
InfoRow(
creationDate = viewState.task.creationDate,
modificationDate = viewState.task.modificationDate,
completionDate = viewState.task.completionDate,
locale = locale,
)
CalendarControlSet.TAG -> AndroidViewBinding(TaskEditCalendarBinding::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")
}
}
}
@ -296,7 +243,7 @@ class TaskEditFragment : Fragment() {
private suspend fun process(event: TaskEditEvent) {
when (event) {
is TaskEditEvent.Discard ->
if (event.id == editViewModel.task.id) {
if (event.id == editViewModel.viewState.value.task.id) {
editViewModel.discard()
}
}
@ -346,8 +293,7 @@ class TaskEditFragment : Fragment() {
}
REQUEST_CODE_PICK_CALENDAR -> {
if (resultCode == Activity.RESULT_OK) {
editViewModel.selectedCalendar.value =
data!!.getStringExtra(CalendarPicker.EXTRA_CALENDAR_ID)
editViewModel.setCalendar(data!!.getStringExtra(CalendarPicker.EXTRA_CALENDAR_ID))
}
}
else -> super.onActivityResult(requestCode, resultCode, data)
@ -356,26 +302,22 @@ class TaskEditFragment : Fragment() {
@Composable
private fun TitleRow(
viewState: TaskEditViewModel.ViewState,
requestFocus: Boolean,
) {
val isComplete = editViewModel.completed.collectAsStateWithLifecycle().value
val recurrence = editViewModel.recurrence.collectAsStateWithLifecycle().value
val isRecurring = remember(recurrence) {
!recurrence.isNullOrBlank()
}
org.tasks.compose.edit.TitleRow(
text = editViewModel.title,
onChanged = { text -> editViewModel.title = text.toString().trim { it <= ' ' } },
linkify = if (preferences.linkify) linkify else null,
TitleRow(
text = viewState.task.title,
onChanged = { text -> editViewModel.setTitle(text.toString().trim { it <= ' ' }) },
linkify = if (viewState.linkify) linkify else null,
markdownProvider = markdownProvider,
isCompleted = isComplete,
isRecurring = isRecurring,
priority = editViewModel.priority.collectAsStateWithLifecycle().value,
isCompleted = viewState.isCompleted,
isRecurring = viewState.task.isRecurring,
priority = viewState.task.priority,
onComplete = {
if (isComplete) {
editViewModel.completed.value = false
if (viewState.isCompleted) {
editViewModel.setComplete(false)
} else {
editViewModel.completed.value = true
editViewModel.setComplete(true)
lifecycleScope.launch {
save()
}
@ -387,6 +329,7 @@ class TaskEditFragment : Fragment() {
@Composable
private fun DueDateRow() {
val viewState = editViewModel.viewState.collectAsStateWithLifecycle().value
val dueDate = editViewModel.dueDate.collectAsStateWithLifecycle().value
val context = LocalContext.current
DueDateRow(
@ -413,73 +356,13 @@ class TaskEditFragment : Fragment() {
R.string.p_auto_dismiss_datetime_edit_screen,
false
),
hideNoDate = editViewModel.recurrence.value?.isNotBlank() == true,
hideNoDate = viewState.task.isRecurring,
)
.show(parentFragmentManager, FRAG_TAG_DATE_PICKER)
}
)
}
@Composable
private fun PriorityRow() {
PriorityRow(
priority = editViewModel.priority.collectAsStateWithLifecycle().value,
onChangePriority = { editViewModel.priority.value = it },
)
}
@Composable
private fun DescriptionRow() {
DescriptionRow(
text = editViewModel.description.stripCarriageReturns(),
onChanged = { text -> editViewModel.description = text.toString().trim { it <= ' ' } },
linkify = if (preferences.linkify) linkify else null,
markdownProvider = markdownProvider,
)
}
@Composable
private fun ListRow() {
val list = editViewModel.selectedList.collectAsStateWithLifecycle().value
ListRow(
list = list,
colorProvider = { chipProvider.getColor(it) },
onClick = {
listPickerLauncher.launch(
context = context,
selectedFilter = list,
listsOnly = true
)
}
)
}
@Composable
fun CreationRow() {
InfoRow(
creationDate = editViewModel.creationDate,
modificationDate = editViewModel.modificationDate,
completionDate = editViewModel.completionDate,
locale = locale,
)
}
@Composable
fun Comments() {
CommentsRow(
comments = userActivityDao
.watchComments(editViewModel.task.uuid)
.collectAsStateWithLifecycle(emptyList())
.value,
deleteComment = {
lifecycleScope.launch {
userActivityDao.delete(it)
}
},
openImage = { FileHelper.startActionView(requireActivity(), it) }
)
}
companion object {
const val EXTRA_TASK = "extra_task"
const val EXTRA_LIST = "extra_list"

@ -15,7 +15,6 @@ import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.tasks.R
import org.tasks.Strings
@ -34,7 +33,7 @@ class FilesControlSet : TaskEditControlFragment() {
@Inject lateinit var preferences: Preferences
override fun createView(savedInstanceState: Bundle?) {
val task = viewModel.task
val task = viewModel.viewState.value.task
if (savedInstanceState == null) {
if (task.hasTransitory(TaskAttachment.KEY)) {
for (uri in (task.getTransitory<ArrayList<Uri>>(TaskAttachment.KEY))!!) {
@ -47,15 +46,16 @@ class FilesControlSet : TaskEditControlFragment() {
override fun bind(parent: ViewGroup?): View =
(parent as ComposeView).apply {
setContent {
val viewState = viewModel.viewState.collectAsStateWithLifecycle().value
AttachmentRow(
attachments = viewModel.selectedAttachments.collectAsStateWithLifecycle().value,
attachments = viewState.attachments,
openAttachment = {
FileHelper.startActionView(
requireActivity(),
if (Strings.isNullOrEmpty(it.uri)) null else Uri.parse(it.uri)
)
},
deleteAttachment = this@FilesControlSet::deleteAttachment,
deleteAttachment = { viewModel.setAttachments(viewState.attachments - it) },
addAttachment = {
AddAttachmentDialog.newAddAttachmentDialog(this@FilesControlSet)
.show(parentFragmentManager, FRAG_TAG_ADD_ATTACHMENT_DIALOG)
@ -90,12 +90,6 @@ class FilesControlSet : TaskEditControlFragment() {
}
}
private fun deleteAttachment(attachment: TaskAttachment) {
viewModel.selectedAttachments.update {
it.minus(attachment)
}
}
private fun copyToAttachmentDirectory(input: Uri?) {
newAttachment(FileHelper.copyToUri(requireContext(), preferences.attachmentsDirectory!!, input!!))
}
@ -107,11 +101,9 @@ class FilesControlSet : TaskEditControlFragment() {
)
lifecycleScope.launch {
taskAttachmentDao.insert(attachment)
viewModel.selectedAttachments.update {
it.plus(
taskAttachmentDao.getAttachment(attachment.remoteId) ?: return@launch
)
}
viewModel.setAttachments(
viewModel.viewState.value.attachments +
(taskAttachmentDao.getAttachment(attachment.remoteId) ?: return@launch))
}
}

@ -19,14 +19,11 @@ import net.fortuna.ical4j.model.WeekDay
import org.tasks.R
import org.tasks.compose.edit.RepeatRow
import org.tasks.data.dao.CaldavDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.filters.CaldavFilter
import org.tasks.repeats.BasicRecurrenceDialog
import org.tasks.repeats.RecurrenceUtils.newRecur
import org.tasks.repeats.RepeatRuleToString
import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import org.tasks.time.startOfDay
import org.tasks.ui.TaskEditControlFragment
import javax.inject.Inject
@ -39,10 +36,7 @@ class RepeatControlSet : TaskEditControlFragment() {
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(currentTimeMillis().startOfDay())
}
viewModel.setRecurrence(result)
}
} else {
super.onActivityResult(requestCode, resultCode, data)
@ -50,7 +44,8 @@ class RepeatControlSet : TaskEditControlFragment() {
}
private fun onDueDateChanged() {
viewModel.recurrence.value?.takeIf { it.isNotBlank() }?.let { recurrence ->
// TODO: move to view model
viewModel.viewState.value.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]
@ -67,7 +62,7 @@ class RepeatControlSet : TaskEditControlFragment() {
it.clear()
it.add(WeekDay(dateTime.weekDay, num))
}
viewModel.recurrence.value = recur.toString()
viewModel.setRecurrence(recur.toString())
}
}
}
@ -83,31 +78,22 @@ class RepeatControlSet : TaskEditControlFragment() {
override fun bind(parent: ViewGroup?): View =
(parent as ComposeView).apply {
setContent {
val viewState = viewModel.viewState.collectAsStateWithLifecycle().value
RepeatRow(
recurrence = viewModel.recurrence.collectAsStateWithLifecycle().value?.let {
repeatRuleToString.toString(it)
},
repeatAfterCompletion = viewModel.repeatAfterCompletion.collectAsStateWithLifecycle().value,
recurrence = viewState.task.recurrence?.let { repeatRuleToString.toString(it) },
repeatFrom = viewState.task.repeatFrom,
onClick = {
val accountType = viewModel.selectedList.value
.let {
when (it) {
is CaldavFilter -> it.account
else -> null
}
}
?.accountType
?: CaldavAccount.TYPE_LOCAL
val accountType = viewState.list.account.accountType
BasicRecurrenceDialog.newBasicRecurrenceDialog(
target = this@RepeatControlSet,
rc = REQUEST_RECURRENCE,
rrule = viewModel.recurrence.value,
rrule = viewState.task.recurrence,
dueDate = viewModel.dueDate.value,
accountType = accountType,
)
.show(parentFragmentManager, FRAG_TAG_BASIC_RECURRENCE)
},
onRepeatFromChanged = { viewModel.repeatAfterCompletion.value = it }
onRepeatFromChanged = { viewModel.setRepeatFrom(it) }
)
}
}

@ -16,6 +16,7 @@ import org.tasks.data.createDueDate
import org.tasks.data.entity.Alarm
import org.tasks.data.entity.Alarm.Companion.TYPE_SNOOZE
import org.tasks.data.entity.Task
import org.tasks.data.entity.Task.RepeatFrom
import org.tasks.data.setRecurrence
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.repeats.RecurrenceUtils.newRecur
@ -39,7 +40,7 @@ class RepeatTaskHelper @Inject constructor(
if (recurrence.isNullOrBlank()) {
return
}
val repeatAfterCompletion = task.repeatAfterCompletion()
val repeatAfterCompletion = task.repeatFrom == RepeatFrom.COMPLETION_DATE
val newDueDate: Long
val rrule: Recur
val count: Int

@ -5,13 +5,15 @@ import android.content.Intent
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.core.content.IntentCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.update
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toPersistentSet
import org.tasks.R
import org.tasks.compose.edit.TagsRow
import org.tasks.data.entity.TagData
import org.tasks.tags.TagPickerActivity
import org.tasks.themes.TasksTheme
import org.tasks.ui.ChipProvider
import org.tasks.ui.TaskEditControlFragment
import javax.inject.Inject
@ -21,21 +23,21 @@ class TagsControlSet : TaskEditControlFragment() {
@Inject lateinit var chipProvider: ChipProvider
private fun onRowClick() {
val intent = Intent(context, TagPickerActivity::class.java)
intent.putParcelableArrayListExtra(TagPickerActivity.EXTRA_SELECTED, viewModel.selectedTags.value)
startActivityForResult(intent, REQUEST_TAG_PICKER_ACTIVITY)
}
override fun bind(parent: ViewGroup?): View =
(parent as ComposeView).apply {
setContent {
val viewState = viewModel.viewState.collectAsStateWithLifecycle().value
TagsRow(
tags = viewModel.selectedTags.collectAsStateWithLifecycle().value,
tags = viewState.tags,
colorProvider = { chipProvider.getColor(it) },
onClick = this@TagsControlSet::onRowClick,
onClear = { tag ->
viewModel.selectedTags.update { ArrayList(it.minus(tag)) }
onClick = {
val intent = Intent(context, TagPickerActivity::class.java)
intent.putParcelableArrayListExtra(TagPickerActivity.EXTRA_SELECTED, ArrayList(viewState.tags))
startActivityForResult(intent, REQUEST_TAG_PICKER_ACTIVITY)
},
onClear = { viewModel.setTags(viewState.tags.minus(it)) },
)
}
}
@ -45,9 +47,16 @@ 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 =
data.getParcelableArrayListExtra(TagPickerActivity.EXTRA_SELECTED)
?: ArrayList()
viewModel.setTags(
IntentCompat
.getParcelableArrayListExtra(
data,
TagPickerActivity.EXTRA_SELECTED,
TagData::class.java
)
?.toPersistentSet()
?: persistentSetOf()
)
}
} else {
super.onActivityResult(requestCode, resultCode, data)

@ -104,7 +104,7 @@ class TimerControlSet : TaskEditControlFragment() {
private fun timerActive() = viewModel.timerStarted.value > 0
private suspend fun stopTimer(): Task {
val model = viewModel.task
val model = viewModel.viewState.value.task
timerPlugin.stopTimer(model)
val elapsedTime = DateUtils.formatElapsedTime(model.elapsedSeconds.toLong())
viewModel.addComment(String.format(
@ -119,7 +119,7 @@ class TimerControlSet : TaskEditControlFragment() {
}
private suspend fun startTimer(): Task {
val model = viewModel.task
val model = viewModel.viewState.value.task
timerPlugin.startTimer(model)
viewModel.addComment(String.format(
"%s %s",

@ -99,8 +99,9 @@ class ReminderControlSet : TaskEditControlFragment() {
replace?.let { viewModel.removeAlarm(it) }
viewModel.addAlarm(Alarm(time = timestamp, type = TYPE_DATE_TIME))
}
val viewState = viewModel.viewState.collectAsStateWithLifecycle().value
AlarmRow(
alarms = viewModel.selectedAlarms.collectAsStateWithLifecycle().value,
alarms = viewState.alarms,
hasNotificationPermissions = hasReminderPermissions &&
(notificationPermissions == null || notificationPermissions.status == PermissionStatus.Granted),
fixNotificationPermissions = {

@ -36,7 +36,11 @@ class StartDateControlSet : TaskEditControlFragment() {
override fun createView(savedInstanceState: Bundle?) {
if (savedInstanceState == null) {
vm.init(viewModel.dueDate.value, viewModel.startDate.value, viewModel.isNew)
vm.init(
dueDate = viewModel.dueDate.value,
startDate = viewModel.startDate.value,
isNew = viewModel.viewState.value.isNew
)
}
lifecycleScope.launchWhenResumed {
viewModel.dueDate.collect {

@ -46,6 +46,8 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.content.res.ResourcesCompat
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.entity.Alarm
@ -485,7 +487,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,
@ -637,7 +639,7 @@ fun AddReminderDialog() =
TasksTheme {
AddAlarmDialog(
viewState = ViewState(showAddAlarm = true),
existingAlarms = emptyList(),
existingAlarms = persistentSetOf(),
addAlarm = {},
addRandom = {},
addCustom = {},

@ -22,6 +22,8 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
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
@ -38,7 +40,7 @@ fun AlarmRow(
vm: ReminderControlSetViewModel = viewModel(),
hasNotificationPermissions: Boolean,
fixNotificationPermissions: () -> Unit,
alarms: List<Alarm>,
alarms: ImmutableSet<Alarm>,
ringMode: Int,
addAlarm: (Alarm) -> Unit,
deleteAlarm: (Alarm) -> Unit,
@ -119,7 +121,7 @@ fun AlarmRow(
@Composable
fun Alarms(
alarms: List<Alarm>,
alarms: ImmutableSet<Alarm>,
ringMode: Int,
replaceAlarm: (Alarm) -> Unit,
addAlarm: () -> Unit,
@ -194,7 +196,7 @@ private fun AlarmRow(
fun NoAlarms() {
TasksTheme {
AlarmRow(
alarms = emptyList(),
alarms = persistentSetOf(),
ringMode = 0,
addAlarm = {},
deleteAlarm = {},
@ -212,7 +214,7 @@ fun NoAlarms() {
fun PermissionDenied() {
TasksTheme {
AlarmRow(
alarms = emptyList(),
alarms = persistentSetOf(),
ringMode = 0,
addAlarm = {},
deleteAlarm = {},

@ -53,6 +53,8 @@ import coil.decode.VideoFrameDecoder
import coil.request.CachePolicy
import coil.request.ImageRequest
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
@ -65,7 +67,7 @@ private val SIZE = 128.dp
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun AttachmentRow(
attachments: List<TaskAttachment>,
attachments: ImmutableSet<TaskAttachment>,
openAttachment: (TaskAttachment) -> Unit,
deleteAttachment: (TaskAttachment) -> Unit,
addAttachment: () -> Unit,
@ -260,7 +262,7 @@ fun BoxScope.DeleteAttachment(
fun NoAttachments() {
TasksTheme {
AttachmentRow(
attachments = emptyList(),
attachments = persistentSetOf(),
openAttachment = {},
deleteAttachment = {},
addAttachment = {},
@ -274,7 +276,7 @@ fun NoAttachments() {
fun AttachmentPreview() {
TasksTheme {
AttachmentRow(
attachments = listOf(
attachments = persistentSetOf(
TaskAttachment(
uri = "file://attachment.txt",
name = "attachment.txt",

@ -28,21 +28,22 @@ import androidx.compose.ui.unit.dp
import org.tasks.R
import org.tasks.compose.DisabledText
import org.tasks.compose.TaskEditRow
import org.tasks.data.entity.Task
import org.tasks.themes.TasksTheme
@Composable
fun RepeatRow(
recurrence: String?,
repeatAfterCompletion: Boolean,
@Task.RepeatFrom repeatFrom: Int,
onClick: () -> Unit,
onRepeatFromChanged: (Boolean) -> Unit,
onRepeatFromChanged: (@Task.RepeatFrom Int) -> Unit,
) {
TaskEditRow(
iconRes = R.drawable.ic_outline_repeat_24px,
content = {
Repeat(
recurrence = recurrence,
repeatFromCompletion = repeatAfterCompletion,
repeatFrom = repeatFrom,
onRepeatFromChanged = onRepeatFromChanged,
)
},
@ -53,8 +54,8 @@ fun RepeatRow(
@Composable
fun Repeat(
recurrence: String?,
repeatFromCompletion: Boolean,
onRepeatFromChanged: (Boolean) -> Unit,
repeatFrom: @Task.RepeatFrom Int,
onRepeatFromChanged: (@Task.RepeatFrom Int) -> Unit,
) {
Column {
Spacer(modifier = Modifier.height(20.dp))
@ -77,10 +78,10 @@ fun Repeat(
var expanded by remember { mutableStateOf(false) }
Text(
text = stringResource(
id = if (repeatFromCompletion)
R.string.repeat_type_completion
else
R.string.repeat_type_due
id = when (repeatFrom) {
Task.RepeatFrom.COMPLETION_DATE -> R.string.repeat_type_completion
else -> R.string.repeat_type_due
}
),
style = MaterialTheme.typography.bodyLarge.copy(
textDecoration = TextDecoration.Underline,
@ -92,7 +93,7 @@ fun Repeat(
DropdownMenuItem(
onClick = {
expanded = false
onRepeatFromChanged(false)
onRepeatFromChanged(Task.RepeatFrom.DUE_DATE)
},
text = {
Text(
@ -104,7 +105,7 @@ fun Repeat(
DropdownMenuItem(
onClick = {
expanded = false
onRepeatFromChanged(true)
onRepeatFromChanged(Task.RepeatFrom.COMPLETION_DATE)
},
text = {
Text(
@ -128,7 +129,7 @@ fun RepeatPreview() {
TasksTheme {
RepeatRow(
recurrence = "Repeats weekly on Mon, Tue, Wed, Thu, Fri",
repeatAfterCompletion = false,
repeatFrom = Task.RepeatFrom.DUE_DATE,
onClick = {},
onRepeatFromChanged = {},
)
@ -143,7 +144,7 @@ fun NoRepeatPreview() {
TasksTheme {
RepeatRow(
recurrence = null,
repeatAfterCompletion = false,
repeatFrom = Task.RepeatFrom.DUE_DATE,
onClick = {},
onRepeatFromChanged = {},
)

@ -7,6 +7,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
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.TasksTheme
@Composable
fun TagsRow(
tags: List<TagData>,
tags: ImmutableSet<TagData>,
colorProvider: (Int) -> Int,
onClick: () -> Unit,
onClear: (TagData) -> Unit,
@ -56,7 +58,7 @@ fun TagsRow(
fun NoTags() {
TasksTheme {
TagsRow(
tags = emptyList(),
tags = persistentSetOf(),
colorProvider = { 0 },
onClick = {},
onClear = {},
@ -70,7 +72,7 @@ fun NoTags() {
fun SingleTag() {
TasksTheme {
TagsRow(
tags = listOf(
tags = persistentSetOf(
TagData(
name = "Home",
icon = "home",
@ -89,7 +91,7 @@ fun SingleTag() {
fun BunchOfTags() {
TasksTheme {
TagsRow(
tags = listOf(
tags = persistentSetOf(
TagData(name = "One"),
TagData(name = "Two"),
TagData(name = "Three"),
@ -108,7 +110,7 @@ fun BunchOfTags() {
fun TagWithReallyLongName() {
TasksTheme {
TagsRow(
tags = listOf(
tags = persistentSetOf(
TagData(
name = "This is a tag with a really really long name",
icon = "home",

@ -0,0 +1,141 @@
package org.tasks.compose.edit
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material.icons.outlined.Clear
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material.icons.outlined.Save
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.viewinterop.AndroidViewBinding
import com.todoroo.astrid.activity.TaskEditFragment.Companion.gesturesDisabled
import org.tasks.R
import org.tasks.compose.BeastModeBanner
import org.tasks.data.entity.UserActivity
import org.tasks.databinding.TaskEditCommentBarBinding
import org.tasks.extensions.Context.findActivity
import org.tasks.files.FileHelper
import org.tasks.themes.TasksTheme
import org.tasks.ui.TaskEditViewModel
@OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class)
@Composable
fun TaskEditScreen(
viewState: TaskEditViewModel.ViewState,
comments: List<UserActivity>,
save: () -> Unit,
discard: () -> Unit,
onBackPressed: () -> Unit,
delete: () -> Unit,
openBeastModeSettings: () -> Unit,
dismissBeastMode: () -> Unit,
deleteComment: (UserActivity) -> Unit,
content: @Composable (Int) -> Unit,
) {
Scaffold(
topBar = {
TopAppBar(
navigationIcon = {
if (viewState.isReadOnly) {
IconButton(onClick = { onBackPressed() }) {
Icon(
imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
contentDescription = stringResource(R.string.back)
)
}
} else {
IconButton(onClick = { save() }) {
Icon(
imageVector = Icons.Outlined.Save,
contentDescription = stringResource(R.string.save)
)
}
}
},
title = {},
actions = {
if (viewState.isReadOnly) {
return@TopAppBar
}
if (!viewState.isNew) {
IconButton(onClick = { delete() }) {
Icon(
imageVector = Icons.Outlined.Delete,
contentDescription = stringResource(R.string.delete_task),
)
}
}
if (viewState.backButtonSavesTask) {
IconButton(onClick = { discard() }) {
Icon(
imageVector = Icons.Outlined.Clear,
contentDescription = stringResource(R.string.menu_discard_changes),
)
}
}
},
)
},
bottomBar = {
if (viewState.showComments && !viewState.isReadOnly) {
AndroidViewBinding(TaskEditCommentBarBinding::inflate)
}
},
) { paddingValues ->
Column(
modifier = Modifier
.gesturesDisabled(viewState.isReadOnly)
.padding(paddingValues)
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
viewState.displayOrder.forEach { tag ->
content(tag)
HorizontalDivider()
}
if (viewState.showComments) {
val context = LocalContext.current
CommentsRow(
comments = comments,
deleteComment = deleteComment,
openImage = {
val activity = context.findActivity() ?: return@CommentsRow
FileHelper.startActionView(activity, it)
}
)
}
BeastModeBanner(
visible = viewState.showBeastModeHint,
showSettings = openBeastModeSettings,
dismiss = dismissBeastMode,
)
}
}
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES)
@Composable
fun TaskEditScreenPreview() {
TasksTheme {
// TaskEditScreen(
//
// )
}
}

@ -3,11 +3,12 @@ package org.tasks.data
import android.content.Context
import android.net.Uri
import org.tasks.data.entity.Place
import org.tasks.extensions.Context.findActivity
import org.tasks.extensions.Context.openUri
import org.tasks.location.MapPosition
fun Place.open(context: Context?) =
context?.openUri("geo:$latitude,$longitude?q=${Uri.encode(displayName)}")
context?.findActivity()?.openUri("geo:$latitude,$longitude?q=${Uri.encode(displayName)}")
val Place.mapPosition: MapPosition
get() = MapPosition(latitude, longitude)

@ -17,7 +17,6 @@ import android.widget.LinearLayout
import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.todoroo.andlib.utility.AndroidUtilities
import dagger.hilt.android.AndroidEntryPoint
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
@ -71,7 +70,7 @@ class CommentBarFragment : Fragment() {
commentField.maxLines = Int.MAX_VALUE
if (
preferences.getBoolean(R.string.p_show_task_edit_comments, false) &&
viewModel.isWritable
!viewModel.viewState.value.isReadOnly
) {
commentBar.visibility = View.VISIBLE
}

@ -1,67 +0,0 @@
package org.tasks.fragments
import android.content.Context
import com.todoroo.astrid.activity.BeastModePreferences
import com.todoroo.astrid.files.FilesControlSet
import com.todoroo.astrid.repeats.RepeatControlSet
import com.todoroo.astrid.tags.TagsControlSet
import com.todoroo.astrid.timers.TimerControlSet
import com.todoroo.astrid.ui.ReminderControlSet
import com.todoroo.astrid.ui.StartDateControlSet
import dagger.hilt.android.qualifiers.ActivityContext
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(
@ActivityContext context: Context,
preferences: Preferences?
) {
val controlSetFragments: MutableMap<String, Int> = LinkedHashMap()
val displayOrder: List<String>
var visibleSize = 0
init {
displayOrder = BeastModePreferences.constructOrderedControlList(preferences, context)
val hideAlwaysTrigger = context.getString(R.string.TEA_ctrl_hide_section_pref)
visibleSize = 0
while (visibleSize < displayOrder.size) {
if (displayOrder[visibleSize] == hideAlwaysTrigger) {
displayOrder.removeAt(visibleSize)
break
}
visibleSize++
}
for (resId in TASK_EDIT_CONTROL_SET_FRAGMENTS) {
controlSetFragments[context.getString(resId)] = resId
}
}
companion object {
val TAG_DESCRIPTION = R.string.TEA_ctrl_notes_pref
val TAG_CREATION = R.string.TEA_ctrl_creation_date
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
private val TASK_EDIT_CONTROL_SET_FRAGMENTS = intArrayOf(
TAG_DUE_DATE,
TimerControlSet.TAG,
TAG_DESCRIPTION,
CalendarControlSet.TAG,
TAG_PRIORITY,
StartDateControlSet.TAG,
ReminderControlSet.TAG,
LocationControlSet.TAG,
FilesControlSet.TAG,
TagsControlSet.TAG,
RepeatControlSet.TAG,
TAG_CREATION,
TAG_LIST,
SubtaskControlSet.TAG
)
}
}

@ -7,6 +7,7 @@ import android.provider.CalendarContract
import android.view.View
import android.view.ViewGroup
import android.widget.Toast.LENGTH_SHORT
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.todoroo.astrid.activity.TaskEditFragment
@ -37,17 +38,18 @@ class CalendarControlSet : TaskEditControlFragment() {
}
}
if (!canAccessCalendars) {
viewModel.selectedCalendar.value = null
viewModel.setCalendar(null)
}
}
override fun bind(parent: ViewGroup?): View =
(parent as ComposeView).apply {
setContent {
val viewState = viewModel.viewState.collectAsStateWithLifecycle().value
CalendarRow(
eventUri = viewModel.eventUri.collectAsStateWithLifecycle().value,
selectedCalendar = viewModel.selectedCalendar.collectAsStateWithLifecycle().value?.let {
calendarProvider.getCalendar(it)?.name
selectedCalendar = remember (viewState.calendar) {
calendarProvider.getCalendar(viewState.calendar)?.name
},
onClick = {
if (viewModel.eventUri.value.isNullOrBlank()) {
@ -55,7 +57,7 @@ class CalendarControlSet : TaskEditControlFragment() {
.newCalendarPicker(
requireParentFragment(),
TaskEditFragment.REQUEST_CODE_PICK_CALENDAR,
viewModel.selectedCalendar.value,
viewState.calendar,
)
.show(
requireParentFragment().parentFragmentManager,
@ -66,7 +68,7 @@ class CalendarControlSet : TaskEditControlFragment() {
}
},
clear = {
viewModel.selectedCalendar.value = null
viewModel.setCalendar(null)
viewModel.eventUri.value = null
}
)

@ -7,6 +7,7 @@ import android.os.Parcelable
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.core.content.IntentCompat
import androidx.core.util.Pair
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.google.accompanist.permissions.ExperimentalPermissionsApi
@ -37,60 +38,58 @@ class LocationControlSet : TaskEditControlFragment() {
@Inject lateinit var dialogBuilder: DialogBuilder
@Inject lateinit var permissionChecker: PermissionChecker
private fun setLocation(location: Location?) {
viewModel.selectedLocation.value = location
}
private fun onRowClick() {
val location = viewModel.selectedLocation.value
if (location == null) {
chooseLocation()
} else {
val options: MutableList<Pair<Int, () -> Unit>> = ArrayList()
options.add(Pair.create(R.string.open_map) { location.open(activity) })
if (!isNullOrEmpty(location.phone)) {
options.add(Pair.create(R.string.action_call) { call() })
}
if (!isNullOrEmpty(location.url)) {
options.add(Pair.create(R.string.visit_website) { openWebsite() })
}
options.add(Pair.create(R.string.choose_new_location) { chooseLocation() })
options.add(Pair.create(R.string.delete) { setLocation(null) })
val items = options.map { requireContext().getString(it.first!!) }
dialogBuilder
.newDialog(location.displayName)
.setItems(items) { _, which: Int ->
options[which].second!!()
}
.show()
}
private fun showGeofenceOptions() {
val dialog = GeofenceDialog.newGeofenceDialog(viewModel.viewState.value.location)
dialog.setTargetFragment(this, REQUEST_GEOFENCE_DETAILS)
dialog.show(parentFragmentManager, FRAG_TAG_LOCATION_DIALOG)
}
private fun chooseLocation() {
val intent = Intent(activity, LocationPickerActivity::class.java)
viewModel.selectedLocation.value?.let {
viewModel.viewState.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)
dialog.setTargetFragment(this, REQUEST_GEOFENCE_DETAILS)
dialog.show(parentFragmentManager, FRAG_TAG_LOCATION_DIALOG)
}
@OptIn(ExperimentalPermissionsApi::class)
override fun bind(parent: ViewGroup?): View =
(parent as ComposeView).apply {
setContent {
val viewState = viewModel.viewState.collectAsStateWithLifecycle().value
val hasPermissions =
rememberMultiplePermissionsState(permissions = backgroundPermissions())
.allPermissionsGranted
LocationRow(
location = viewModel.selectedLocation.collectAsStateWithLifecycle().value,
location = viewState.location,
hasPermissions = hasPermissions,
onClick = this@LocationControlSet::onRowClick,
onClick = {
viewState.location
?.let { location ->
val options: MutableList<Pair<Int, () -> Unit>> = ArrayList()
options.add(Pair.create(R.string.open_map) { location.open(activity) })
if (!isNullOrEmpty(location.phone)) {
options.add(Pair.create(R.string.action_call) { call() })
}
if (!isNullOrEmpty(location.url)) {
options.add(Pair.create(R.string.visit_website) { openWebsite() })
}
options.add(Pair.create(R.string.choose_new_location) { chooseLocation() })
options.add(Pair.create(R.string.delete) {
viewModel.setLocation(
null
)
})
val items = options.map { requireContext().getString(it.first!!) }
dialogBuilder
.newDialog(location.displayName)
.setItems(items) { _, which: Int ->
options[which].second!!()
}
.show()
}
?: chooseLocation()
},
openGeofenceOptions = {
if (hasPermissions) {
showGeofenceOptions()
@ -109,11 +108,11 @@ class LocationControlSet : TaskEditControlFragment() {
override fun controlId() = TAG
private fun openWebsite() {
viewModel.selectedLocation.value?.let { context?.openUri(it.url) }
viewModel.viewState.value.location?.let { context?.openUri(it.url) }
}
private fun call() {
viewModel.selectedLocation.value?.let {
viewModel.viewState.value.location?.let {
startActivity(Intent(Intent.ACTION_DIAL, Uri.parse("tel:" + it.phone)))
}
}
@ -126,7 +125,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.viewState.value.location
val geofence = if (location == null) {
createGeofence(place.uid, preferences)
} else {
@ -137,14 +136,25 @@ class LocationControlSet : TaskEditControlFragment() {
isDeparture = existing.isDeparture,
)
}
setLocation(Location(geofence, place))
viewModel.setLocation(Location(geofence, place))
}
} else if (requestCode == REQUEST_GEOFENCE_DETAILS) {
if (resultCode == Activity.RESULT_OK) {
setLocation(Location(
data?.getParcelableExtra(GeofenceDialog.EXTRA_GEOFENCE) ?: return,
viewModel.selectedLocation.value?.place ?: return
))
val geofence = data
?.let {
IntentCompat.getParcelableExtra(
it,
GeofenceDialog.EXTRA_GEOFENCE,
Geofence::class.java
)
}
?: return
viewModel.setLocation(
Location(
geofence,
viewModel.viewState.value.location?.place ?: return
)
)
}
} else {
super.onActivityResult(requestCode, resultCode, data)

@ -43,7 +43,7 @@ class SubtaskControlSet : TaskEditControlFragment() {
private val mainViewModel: MainActivityViewModel by activityViewModels()
override fun createView(savedInstanceState: Bundle?) {
viewModel.task.takeIf { it.id > 0 }?.let {
viewModel.viewState.value.task.takeIf { it.id > 0 }?.let {
listViewModel.setFilter(SubtaskFilter(it.id))
}
}
@ -52,46 +52,44 @@ class SubtaskControlSet : TaskEditControlFragment() {
(parent as ComposeView).apply {
listViewModel = ViewModelProvider(requireParentFragment())[TaskListViewModel::class.java]
setContent {
val viewState = viewModel.viewState.collectAsStateWithLifecycle().value
SubtaskRow(
originalFilter = viewModel.originalList,
filter = viewModel.selectedList.collectAsStateWithLifecycle().value,
hasParent = viewModel.hasParent,
existingSubtasks = if (viewModel.isNew) {
originalFilter = viewModel.originalState.list,
filter = viewState.list,
hasParent = viewState.hasParent,
existingSubtasks = if (viewModel.viewState.collectAsStateWithLifecycle().value.isNew) {
TasksResults.Results(SectionedDataSource())
} else {
listViewModel.state.collectAsStateWithLifecycle().value.tasks
},
newSubtasks = viewModel.newSubtasks.collectAsStateWithLifecycle().value,
newSubtasks = viewState.newSubtasks,
openSubtask = this@SubtaskControlSet::openSubtask,
completeExistingSubtask = this@SubtaskControlSet::complete,
toggleSubtask = this@SubtaskControlSet::toggleSubtask,
addSubtask = this@SubtaskControlSet::addSubtask,
addSubtask = {
lifecycleScope.launch {
viewModel.setSubtasks(
viewState.newSubtasks.plus(taskCreator.createWithValues(""))
)
}
},
completeNewSubtask = {
viewModel.newSubtasks.value =
ArrayList(viewModel.newSubtasks.value).apply {
viewModel.setSubtasks(
viewState.newSubtasks.toMutableList().apply {
val modified = it.copy(
completionDate = if (it.isCompleted) 0 else currentTimeMillis()
)
set(indexOf(it), modified)
}
)
},
deleteSubtask = {
viewModel.newSubtasks.value =
ArrayList(viewModel.newSubtasks.value).apply {
remove(it)
}
}
deleteSubtask = { viewModel.setSubtasks(viewState.newSubtasks - 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)
}

@ -7,19 +7,33 @@ import androidx.core.net.toUri
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.todoroo.astrid.activity.BeastModePreferences
import com.todoroo.astrid.activity.TaskEditFragment
import com.todoroo.astrid.alarms.AlarmService
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.files.FilesControlSet
import com.todoroo.astrid.gcal.GCalHelper
import com.todoroo.astrid.repeats.RepeatControlSet
import com.todoroo.astrid.service.TaskCompleter
import com.todoroo.astrid.service.TaskCreator.Companion.getDefaultAlarms
import com.todoroo.astrid.service.TaskDeleter
import com.todoroo.astrid.service.TaskMover
import com.todoroo.astrid.tags.TagsControlSet
import com.todoroo.astrid.timers.TimerControlSet
import com.todoroo.astrid.timers.TimerPlugin
import com.todoroo.astrid.ui.ReminderControlSet
import com.todoroo.astrid.ui.StartDateControlSet
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toPersistentList
import kotlinx.collections.immutable.toPersistentSet
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
@ -94,30 +108,105 @@ class TaskEditViewModel @Inject constructor(
private val alarmDao: AlarmDao,
private val taskAttachmentDao: TaskAttachmentDao,
) : ViewModel() {
data class ViewState(
val task: Task,
val displayOrder: ImmutableList<Int>,
val showBeastModeHint: Boolean,
val showComments: Boolean,
val showKeyboard: Boolean,
val backButtonSavesTask: Boolean,
val isReadOnly: Boolean,
val linkify: Boolean,
val list: CaldavFilter,
val location: Location?,
val tags: ImmutableSet<TagData>,
val calendar: String?,
val attachments: ImmutableSet<TaskAttachment> = persistentSetOf(),
val alarms: ImmutableSet<Alarm>,
val newSubtasks: ImmutableList<Task> = persistentListOf(),
) {
val isNew: Boolean
get() = task.isNew
val hasParent: Boolean
get() = task.parent > 0
val isCompleted: Boolean
get() = task.completionDate > 0
}
private val resources = context.resources
private var cleared = false
val task: Task = savedStateHandle[TaskEditFragment.EXTRA_TASK]!!
val isNew = task.isNew
private val task: Task = savedStateHandle.get<Task>(TaskEditFragment.EXTRA_TASK)
?.let { it.copy(notes = it.notes?.stripCarriageReturns()) }
?: throw IllegalArgumentException("task is null")
private var _originalState: ViewState
val originalState: ViewState
get() = _originalState
private val _viewState = MutableStateFlow(
ViewState(
task = task,
showBeastModeHint = !preferences.shownBeastModeHint,
showComments = preferences.getBoolean(R.string.p_show_task_edit_comments, false),
showKeyboard = task.isNew && task.title.isNullOrBlank(),
backButtonSavesTask = preferences.backButtonSavesTask(),
isReadOnly = task.readOnly,
linkify = preferences.linkify,
list = savedStateHandle[TaskEditFragment.EXTRA_LIST]!!,
location = savedStateHandle[TaskEditFragment.EXTRA_LOCATION],
tags = savedStateHandle.get<ArrayList<TagData>>(TaskEditFragment.EXTRA_TAGS)
?.toPersistentSet()
?: persistentSetOf(),
calendar = if (task.isNew && permissionChecker.canAccessCalendars()) {
preferences.defaultCalendar
} else {
null
},
displayOrder = TASK_EDIT_CONTROL_SET_FRAGMENTS
.associateBy(context::getString) { it }
.let { controlSetStrings ->
BeastModePreferences
.constructOrderedControlList(preferences, context)
.let { items ->
items
.subList(
0,
items.indexOf(context.getString(R.string.TEA_ctrl_hide_section_pref))
)
.also { it.add(0, context.getString(R.string.TEA_ctrl_title)) }
}
.mapNotNull { controlSetStrings[it] }
.toPersistentList()
},
alarms = if (task.isNew) {
ArrayList<Alarm>().apply {
if (task.isNotifyAtStart) {
add(whenStarted(0))
}
if (task.isNotifyAtDeadline) {
add(whenDue(0))
}
if (task.isNotifyAfterDeadline) {
add(whenOverdue(0))
}
if (task.randomReminder > 0) {
add(Alarm(time = task.randomReminder, type = Alarm.TYPE_RANDOM))
}
}
} else {
savedStateHandle[TaskEditFragment.EXTRA_ALARMS]!!
}.toPersistentSet(),
)
)
val viewState: StateFlow<ViewState> = _viewState
var showBeastModeHint = MutableStateFlow(!preferences.shownBeastModeHint)
var creationDate: Long = task.creationDate
var modificationDate: Long = task.modificationDate
var completionDate: Long = task.completionDate
var title: String? = task.title
var completed = MutableStateFlow(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)
@ -131,10 +220,14 @@ class TaskEditViewModel @Inject constructor(
if (addedDueDate) {
val reminderFlags = preferences.defaultReminders
if (reminderFlags.isFlagSet(Task.NOTIFY_AT_DEADLINE)) {
selectedAlarms.value = selectedAlarms.value.plusAlarm(whenDue(task.id))
_viewState.update { state ->
state.copy(alarms = state.alarms.plusAlarm(whenDue(task.id)))
}
}
if (reminderFlags.isFlagSet(Task.NOTIFY_AFTER_DEADLINE)) {
selectedAlarms.value = selectedAlarms.value.plusAlarm(whenOverdue(task.id))
_viewState.update { state ->
state.copy(alarms = state.alarms.plusAlarm(whenOverdue(task.id)))
}
}
}
}
@ -150,51 +243,12 @@ class TaskEditViewModel @Inject constructor(
else -> value.startOfDay()
}
if (addedStartDate && preferences.defaultReminders.isFlagSet(Task.NOTIFY_AT_START)) {
selectedAlarms.value = selectedAlarms.value.plusAlarm(whenStarted(task.id))
}
}
private var originalCalendar: String? = if (isNew && permissionChecker.canAccessCalendars()) {
preferences.defaultCalendar
} else {
null
}
var selectedCalendar = MutableStateFlow(originalCalendar)
val originalList: CaldavFilter = savedStateHandle[TaskEditFragment.EXTRA_LIST]!!
var selectedList = MutableStateFlow(originalList)
private var originalLocation: Location? = savedStateHandle[TaskEditFragment.EXTRA_LOCATION]
var selectedLocation = MutableStateFlow(originalLocation)
private val originalTags: List<TagData> =
savedStateHandle.get<ArrayList<TagData>>(TaskEditFragment.EXTRA_TAGS) ?: emptyList()
val selectedTags = MutableStateFlow(ArrayList(originalTags))
private lateinit var originalAttachments: List<TaskAttachment>
val selectedAttachments = MutableStateFlow(emptyList<TaskAttachment>())
private val originalAlarms: List<Alarm> = if (isNew) {
ArrayList<Alarm>().apply {
if (task.isNotifyAtStart) {
add(whenStarted(0))
}
if (task.isNotifyAtDeadline) {
add(whenDue(0))
}
if (task.isNotifyAfterDeadline) {
add(whenOverdue(0))
}
if (task.randomReminder > 0) {
add(Alarm(time = task.randomReminder, type = Alarm.TYPE_RANDOM))
_viewState.update { state ->
state.copy(alarms = state.alarms.plusAlarm(whenStarted(task.id)))
}
}
} else {
savedStateHandle[TaskEditFragment.EXTRA_ALARMS]!!
}
var selectedAlarms = MutableStateFlow(originalAlarms)
var ringNonstop: Boolean = task.isNotifyModeNonstop
set(value) {
field = value
@ -211,28 +265,12 @@ class TaskEditViewModel @Inject constructor(
}
}
val isReadOnly = task.readOnly
val isWritable = !isReadOnly
fun hasChanges(): Boolean =
(task.title != title || (isNew && title?.isNotBlank() == true)) ||
task.isCompleted != completed.value ||
fun hasChanges(): Boolean {
val viewState = _viewState.value
return originalState != viewState ||
(viewState.isNew && viewState.task.title?.isNotBlank() == true) || // text shared to tasks
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 {
@ -240,58 +278,49 @@ class TaskEditViewModel @Inject constructor(
} ||
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()
task.isNotifyModeFive -> NOTIFY_MODE_FIVE
task.isNotifyModeNonstop -> NOTIFY_MODE_NONSTOP
else -> 0
}
}
@MainThread
suspend fun save(remove: Boolean = true): Boolean = withContext(NonCancellable) {
if (cleared) {
return@withContext false
}
if (!hasChanges() || isReadOnly) {
if (!hasChanges() || viewState.value.isReadOnly) {
discard(remove)
return@withContext false
}
clear(remove)
task.title = if (title.isNullOrBlank()) resources.getString(R.string.no_title) else title
val viewState = _viewState.value
val isNew = viewState.isNew
task.title = if (viewState.task.title.isNullOrBlank()) resources.getString(R.string.no_title) else viewState.task.title
task.dueDate = dueDate.value
task.priority = priority.value
task.notes = description
task.priority = viewState.task.priority
task.notes = viewState.task.notes
task.hideUntil = startDate.value
task.recurrence = recurrence.value
task.repeatFrom = if (repeatAfterCompletion.value) {
Task.RepeatFrom.COMPLETION_DATE
} else {
Task.RepeatFrom.DUE_DATE
}
task.recurrence = viewState.task.recurrence
task.repeatFrom = viewState.task.repeatFrom
task.elapsedSeconds = elapsedSeconds.value
task.estimatedSeconds = estimatedSeconds.value
task.ringFlags = getRingFlags()
applyCalendarChanges()
if (isNew) {
taskDao.createNew(task)
}
if ((isNew && selectedLocation.value != null) || originalLocation != selectedLocation.value) {
originalLocation?.let { location ->
val selectedLocation = _viewState.value.location
if ((isNew && selectedLocation != null) || originalState.location != selectedLocation) {
originalState.location?.let { location ->
if (location.geofence.id > 0) {
locationDao.delete(location.geofence)
geofenceApi.update(location.place)
}
}
selectedLocation.value?.let { location ->
selectedLocation?.let { location ->
val place = location.place
locationDao.insert(
location.geofence.copy(
@ -305,36 +334,45 @@ class TaskEditViewModel @Inject constructor(
task.putTransitory(FORCE_MICROSOFT_SYNC, true)
task.modificationDate = currentTimeMillis()
}
if ((isNew && selectedTags.value.isNotEmpty()) || originalTags.toHashSet() != selectedTags.value.toHashSet()) {
tagDao.applyTags(task, tagDataDao, selectedTags.value)
val selectedTags = _viewState.value.tags
if ((isNew && selectedTags.isNotEmpty()) || originalState.tags.toHashSet() != selectedTags.toHashSet()) {
tagDao.applyTags(task, tagDataDao, selectedTags)
task.putTransitory(FORCE_CALDAV_SYNC, true)
task.modificationDate = currentTimeMillis()
}
if (!task.hasStartDate()) {
selectedAlarms.value = selectedAlarms.value.filterNot { a -> a.type == TYPE_REL_START }
_viewState.update { state ->
state.copy(
alarms = state.alarms.filterNot { it.type == TYPE_REL_START }.toPersistentSet()
)
}
}
if (!task.hasDueDate()) {
selectedAlarms.value = selectedAlarms.value.filterNot { a -> a.type == TYPE_REL_END }
_viewState.update { state ->
state.copy(
alarms = state.alarms.filterNot { it.type == TYPE_REL_END }.toPersistentSet()
)
}
}
if (
selectedAlarms.value.toHashSet() != originalAlarms.toHashSet() ||
(isNew && selectedAlarms.value.isNotEmpty())
(isNew && _viewState.value.alarms.isNotEmpty()) ||
originalState.alarms != _viewState.value.alarms
) {
alarmService.synchronizeAlarms(task.id, selectedAlarms.value.toMutableSet())
alarmService.synchronizeAlarms(task.id, _viewState.value.alarms.toMutableSet())
task.putTransitory(FORCE_CALDAV_SYNC, true)
task.modificationDate = currentTimeMillis()
}
taskDao.save(task, null)
if (isNew || originalList != selectedList.value) {
val selectedList = _viewState.value.list
if (isNew || originalState.list != selectedList) {
task.parent = 0
taskMover.move(listOf(task.id), selectedList.value)
taskMover.move(listOf(task.id), selectedList)
}
for (subtask in newSubtasks.value) {
for (subtask in viewState.newSubtasks) {
if (Strings.isNullOrEmpty(subtask.title)) {
continue
}
@ -344,7 +382,7 @@ class TaskEditViewModel @Inject constructor(
taskDao.createNew(subtask)
alarmDao.insert(subtask.getDefaultAlarms())
firebase?.addTask("subtasks")
val filter = selectedList.value
val filter = selectedList
when {
filter.isGoogleTasks -> {
val googleTask = CaldavTask(
@ -363,7 +401,7 @@ class TaskEditViewModel @Inject constructor(
else -> {
val caldavTask = CaldavTask(
task = subtask.id,
calendar = filter.uuid,
calendar = selectedList.uuid,
)
subtask.parent = task.id
caldavTask.remoteParent = caldavDao.getRemoteIdForTask(task.id)
@ -377,16 +415,13 @@ class TaskEditViewModel @Inject constructor(
}
}
if (
this@TaskEditViewModel::originalAttachments.isInitialized &&
selectedAttachments.value.toHashSet() != originalAttachments.toHashSet()
) {
originalAttachments
.minus(selectedAttachments.value.toSet())
if (originalState.attachments != _viewState.value.attachments) {
originalState.attachments
.minus(_viewState.value.attachments)
.map { it.remoteId }
.let { taskAttachmentDao.delete(task.id, it) }
selectedAttachments.value
.minus(originalAttachments.toSet())
_viewState.value.attachments
.minus(originalState.attachments)
.map {
Attachment(
task = task.id,
@ -397,9 +432,9 @@ class TaskEditViewModel @Inject constructor(
.let { taskAttachmentDao.insert(it) }
}
if (task.isCompleted != completed.value) {
taskCompleter.setComplete(task, completed.value)
if (task.isCompleted) {
if (task.isCompleted != _viewState.value.isCompleted) {
taskCompleter.setComplete(task, _viewState.value.isCompleted)
if (_viewState.value.isCompleted) {
firebase?.completeTask("edit_screen_v2")
}
}
@ -424,7 +459,7 @@ class TaskEditViewModel @Inject constructor(
if (!task.hasDueDate()) {
return
}
selectedCalendar.value?.let {
_viewState.value.calendar?.let {
try {
task.calendarURI = gCalHelper.createTaskEvent(task, it)?.toString()
} catch (e: Exception) {
@ -445,11 +480,11 @@ class TaskEditViewModel @Inject constructor(
}
suspend fun discard(remove: Boolean = true) {
if (isNew) {
if (_viewState.value.isNew) {
timerPlugin.stopTimer(task)
originalAttachments.plus(selectedAttachments.value).toSet().takeIf { it.isNotEmpty() }
?.onEach { FileHelper.delete(context, it.uri.toUri()) }
?.let { taskAttachmentDao.delete(it.toList()) }
(originalState.attachments + _viewState.value.attachments)
.onEach { attachment -> FileHelper.delete(context, attachment.uri.toUri()) }
.let { taskAttachmentDao.delete(it.toList()) }
}
clear(remove)
}
@ -474,14 +509,14 @@ class TaskEditViewModel @Inject constructor(
}
fun removeAlarm(alarm: Alarm) {
selectedAlarms.update { it.minus(alarm) }
_viewState.update { state ->
state.copy(alarms = state.alarms.minus(alarm).toPersistentSet())
}
}
fun addAlarm(alarm: Alarm) {
with (selectedAlarms) {
if (value.none { it.same(alarm) }) {
value = value.plus(alarm)
}
_viewState.update { state ->
state.copy(alarms = state.alarms.plusAlarm(alarm))
}
}
@ -502,26 +537,126 @@ class TaskEditViewModel @Inject constructor(
}
fun hideBeastModeHint(click: Boolean) {
showBeastModeHint.value = false
_viewState.update {
it.copy(showBeastModeHint = false)
}
preferences.shownBeastModeHint = true
firebase?.logEvent(R.string.event_banner_beast, R.string.param_click to click)
}
fun setPriority(priority: Int) {
_viewState.update { state -> state.copy(task = state.task.copy(priority = priority)) }
}
fun setTitle(title: String) {
_viewState.update { state -> state.copy(task = state.task.copy(title = title)) }
}
fun setRecurrence(recurrence: String?) {
_viewState.update { state ->
state.copy(
task = state.task.copy(
recurrence = recurrence,
dueDate = if (recurrence?.isNotBlank() == true && task.dueDate == 0L) {
currentTimeMillis().startOfDay()
} else {
task.dueDate
}
)
)
}
}
fun setDescription(description: String) {
_viewState.update { state -> state.copy(task = state.task.copy(notes = description)) }
}
fun setList(list: CaldavFilter) {
_viewState.update { it.copy(list = list) }
}
fun setTags(tags: Set<TagData>) {
_viewState.update { it.copy(tags = tags.toPersistentSet()) }
}
fun setLocation(location: Location?) {
_viewState.update { it.copy(location = location) }
}
fun setCalendar(calendar: String?) {
_viewState.update { it.copy(calendar = calendar) }
}
fun setAttachments(attachments: Set<TaskAttachment>) {
_viewState.update { it.copy(attachments = attachments.toPersistentSet()) }
}
fun setSubtasks(subtasks: List<Task>) {
_viewState.update { it.copy(newSubtasks = subtasks.toPersistentList()) }
}
fun setComplete(completed: Boolean) {
_viewState.update { state ->
state.copy(
task = task.copy(
completionDate = when {
!completed -> 0
task.isCompleted -> task.completionDate
else -> currentTimeMillis()
}
)
)
}
}
fun setRepeatFrom(repeatFrom: @Task.RepeatFrom Int) {
_viewState.update { state ->
state.copy(task = state.task.copy(repeatFrom = repeatFrom))
}
}
init {
_originalState = _viewState.value.copy()
viewModelScope.launch {
taskAttachmentDao.getAttachments(task.id).let { attachments ->
selectedAttachments.update { attachments }
originalAttachments = attachments
taskAttachmentDao.getAttachments(task.id).toPersistentSet().let { attachments ->
_originalState = _originalState.copy(attachments = attachments)
_viewState.value = _viewState.value.copy(attachments = attachments)
}
}
}
companion object {
// one spark tasks for windows adds these
fun String?.stripCarriageReturns(): String? = this?.replace("\\r\\n?".toRegex(), "\n")
private fun Int.isFlagSet(flag: Int): Boolean = this and flag > 0
private fun List<Alarm>.plusAlarm(alarm: Alarm): List<Alarm> =
if (any { it.same(alarm) }) this else this + alarm
private fun ImmutableSet<Alarm>.plusAlarm(alarm: Alarm): ImmutableSet<Alarm> =
if (any { it.same(alarm) }) this else this.plus(alarm).toPersistentSet()
val TAG_TITLE = R.string.TEA_ctrl_title
val TAG_DESCRIPTION = R.string.TEA_ctrl_notes_pref
val TAG_CREATION = R.string.TEA_ctrl_creation_date
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 TASK_EDIT_CONTROL_SET_FRAGMENTS = intArrayOf(
TAG_TITLE,
TAG_DUE_DATE,
TimerControlSet.TAG,
TAG_DESCRIPTION,
CalendarControlSet.TAG,
TAG_PRIORITY,
StartDateControlSet.TAG,
ReminderControlSet.TAG,
LocationControlSet.TAG,
FilesControlSet.TAG,
TagsControlSet.TAG,
RepeatControlSet.TAG,
TAG_CREATION,
TAG_LIST,
SubtaskControlSet.TAG
)
}
}

@ -258,6 +258,7 @@
<item>@string/TEA_ctrl_hide_section_pref</item>
</string-array>
<string name="TEA_ctrl_title">TEA_ctrl_title</string>
<string name="TEA_ctrl_when_pref">TEA_ctrl_when_pref</string>
<string name="TEA_ctrl_repeat_pref">TEA_ctrl_repeat_pref</string>
<string name="TEA_ctrl_importance_pref">TEA_ctrl_importance_pref</string>

@ -36,11 +36,11 @@ abstract class TagDao(private val database: Database) {
@Delete
abstract suspend fun delete(tags: List<Tag>)
open suspend fun applyTags(task: Task, tagDataDao: TagDataDao, current: List<TagData>) {
open suspend fun applyTags(task: Task, tagDataDao: TagDataDao, current: Collection<TagData>) {
database.withTransaction {
val taskId = task.id
val existing = HashSet(tagDataDao.getTagDataForTask(taskId))
val selected = HashSet<TagData>(current)
val selected = current.toMutableSet()
val added = selected subtract existing
val removed = existing subtract selected
deleteTags(taskId, removed.map { td -> td.remoteId!! })

@ -113,8 +113,6 @@ data class Task @OptIn(ExperimentalSerializationApi::class) constructor(
/** Checks whether this due date has a due time or only a date */
fun hasDueTime(): Boolean = hasDueTime(dueDate)
fun repeatAfterCompletion(): Boolean = repeatFrom == RepeatFrom.COMPLETION_DATE
fun setDueDateAdjustingHideUntil(newDueDate: Long) {
if (dueDate > 0) {
if (hideUntil > 0) {
@ -283,6 +281,7 @@ data class Task @OptIn(ExperimentalSerializationApi::class) constructor(
}
}
@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE)
@Retention(AnnotationRetention.SOURCE)
@IntDef(RepeatFrom.DUE_DATE, RepeatFrom.COMPLETION_DATE)
annotation class RepeatFrom {

Loading…
Cancel
Save