Added TaskEditScreen - WIP

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

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

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

@ -1,12 +1,12 @@
package org.tasks.ui.editviewmodel package org.tasks.ui.editviewmodel
import org.tasks.data.entity.Task
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertFalse import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
import org.tasks.data.entity.Task
import org.tasks.injection.ProductionModule import org.tasks.injection.ProductionModule
import org.tasks.makers.TaskMaker.newTask import org.tasks.makers.TaskMaker.newTask
@ -33,7 +33,7 @@ class TaskEditViewModelTest : BaseTaskEditViewModelTest() {
fun dontSaveTaskTwice() = runBlocking { fun dontSaveTaskTwice() = runBlocking {
setup(newTask()) setup(newTask())
viewModel.priority.value = Task.Priority.HIGH viewModel.setPriority(Task.Priority.HIGH)
assertTrue(save()) assertTrue(save())

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

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

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

@ -19,14 +19,11 @@ import net.fortuna.ical4j.model.WeekDay
import org.tasks.R import org.tasks.R
import org.tasks.compose.edit.RepeatRow import org.tasks.compose.edit.RepeatRow
import org.tasks.data.dao.CaldavDao 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.BasicRecurrenceDialog
import org.tasks.repeats.RecurrenceUtils.newRecur import org.tasks.repeats.RecurrenceUtils.newRecur
import org.tasks.repeats.RepeatRuleToString import org.tasks.repeats.RepeatRuleToString
import org.tasks.time.DateTime import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils2.currentTimeMillis import org.tasks.time.DateTimeUtils2.currentTimeMillis
import org.tasks.time.startOfDay
import org.tasks.ui.TaskEditControlFragment import org.tasks.ui.TaskEditControlFragment
import javax.inject.Inject import javax.inject.Inject
@ -39,10 +36,7 @@ class RepeatControlSet : TaskEditControlFragment() {
if (requestCode == REQUEST_RECURRENCE) { if (requestCode == REQUEST_RECURRENCE) {
if (resultCode == RESULT_OK) { if (resultCode == RESULT_OK) {
val result = data?.getStringExtra(BasicRecurrenceDialog.EXTRA_RRULE) val result = data?.getStringExtra(BasicRecurrenceDialog.EXTRA_RRULE)
viewModel.recurrence.value = result viewModel.setRecurrence(result)
if (result?.isNotBlank() == true && viewModel.dueDate.value == 0L) {
viewModel.setDueDate(currentTimeMillis().startOfDay())
}
} }
} else { } else {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
@ -50,7 +44,8 @@ class RepeatControlSet : TaskEditControlFragment() {
} }
private fun onDueDateChanged() { 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) val recur = newRecur(recurrence)
if (recur.frequency == Recur.Frequency.MONTHLY && recur.dayList.isNotEmpty()) { if (recur.frequency == Recur.Frequency.MONTHLY && recur.dayList.isNotEmpty()) {
val weekdayNum = recur.dayList[0] val weekdayNum = recur.dayList[0]
@ -67,7 +62,7 @@ class RepeatControlSet : TaskEditControlFragment() {
it.clear() it.clear()
it.add(WeekDay(dateTime.weekDay, num)) 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 = override fun bind(parent: ViewGroup?): View =
(parent as ComposeView).apply { (parent as ComposeView).apply {
setContent { setContent {
val viewState = viewModel.viewState.collectAsStateWithLifecycle().value
RepeatRow( RepeatRow(
recurrence = viewModel.recurrence.collectAsStateWithLifecycle().value?.let { recurrence = viewState.task.recurrence?.let { repeatRuleToString.toString(it) },
repeatRuleToString.toString(it) repeatFrom = viewState.task.repeatFrom,
},
repeatAfterCompletion = viewModel.repeatAfterCompletion.collectAsStateWithLifecycle().value,
onClick = { onClick = {
val accountType = viewModel.selectedList.value val accountType = viewState.list.account.accountType
.let {
when (it) {
is CaldavFilter -> it.account
else -> null
}
}
?.accountType
?: CaldavAccount.TYPE_LOCAL
BasicRecurrenceDialog.newBasicRecurrenceDialog( BasicRecurrenceDialog.newBasicRecurrenceDialog(
target = this@RepeatControlSet, target = this@RepeatControlSet,
rc = REQUEST_RECURRENCE, rc = REQUEST_RECURRENCE,
rrule = viewModel.recurrence.value, rrule = viewState.task.recurrence,
dueDate = viewModel.dueDate.value, dueDate = viewModel.dueDate.value,
accountType = accountType, accountType = accountType,
) )
.show(parentFragmentManager, FRAG_TAG_BASIC_RECURRENCE) .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
import org.tasks.data.entity.Alarm.Companion.TYPE_SNOOZE import org.tasks.data.entity.Alarm.Companion.TYPE_SNOOZE
import org.tasks.data.entity.Task import org.tasks.data.entity.Task
import org.tasks.data.entity.Task.RepeatFrom
import org.tasks.data.setRecurrence import org.tasks.data.setRecurrence
import org.tasks.date.DateTimeUtils.newDateTime import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.repeats.RecurrenceUtils.newRecur import org.tasks.repeats.RecurrenceUtils.newRecur
@ -39,7 +40,7 @@ class RepeatTaskHelper @Inject constructor(
if (recurrence.isNullOrBlank()) { if (recurrence.isNullOrBlank()) {
return return
} }
val repeatAfterCompletion = task.repeatAfterCompletion() val repeatAfterCompletion = task.repeatFrom == RepeatFrom.COMPLETION_DATE
val newDueDate: Long val newDueDate: Long
val rrule: Recur val rrule: Recur
val count: Int val count: Int

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

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

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

@ -36,7 +36,11 @@ class StartDateControlSet : TaskEditControlFragment() {
override fun createView(savedInstanceState: Bundle?) { override fun createView(savedInstanceState: Bundle?) {
if (savedInstanceState == null) { 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 { lifecycleScope.launchWhenResumed {
viewModel.dueDate.collect { viewModel.dueDate.collect {

@ -46,6 +46,8 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.res.ResourcesCompat import androidx.core.content.res.ResourcesCompat
import com.todoroo.astrid.ui.ReminderControlSetViewModel.ViewState import com.todoroo.astrid.ui.ReminderControlSetViewModel.ViewState
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentSetOf
import kotlinx.coroutines.android.awaitFrame import kotlinx.coroutines.android.awaitFrame
import org.tasks.R import org.tasks.R
import org.tasks.data.entity.Alarm import org.tasks.data.entity.Alarm
@ -485,7 +487,7 @@ fun BodyText(modifier: Modifier = Modifier, text: String) {
@Composable @Composable
fun AddAlarmDialog( fun AddAlarmDialog(
viewState: ViewState, viewState: ViewState,
existingAlarms: List<Alarm>, existingAlarms: ImmutableSet<Alarm>,
addAlarm: (Alarm) -> Unit, addAlarm: (Alarm) -> Unit,
addRandom: () -> Unit, addRandom: () -> Unit,
addCustom: () -> Unit, addCustom: () -> Unit,
@ -637,11 +639,11 @@ fun AddReminderDialog() =
TasksTheme { TasksTheme {
AddAlarmDialog( AddAlarmDialog(
viewState = ViewState(showAddAlarm = true), viewState = ViewState(showAddAlarm = true),
existingAlarms = emptyList(), existingAlarms = persistentSetOf(),
addAlarm = {}, addAlarm = {},
addRandom = {}, addRandom = {},
addCustom = {}, addCustom = {},
pickDateAndTime = {}, pickDateAndTime = {},
dismiss = {}, dismiss = {},
) )
} }

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

@ -53,6 +53,8 @@ import coil.decode.VideoFrameDecoder
import coil.request.CachePolicy import coil.request.CachePolicy
import coil.request.ImageRequest import coil.request.ImageRequest
import com.todoroo.andlib.utility.AndroidUtilities import com.todoroo.andlib.utility.AndroidUtilities
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentSetOf
import org.tasks.R import org.tasks.R
import org.tasks.compose.DisabledText import org.tasks.compose.DisabledText
import org.tasks.compose.TaskEditRow import org.tasks.compose.TaskEditRow
@ -65,7 +67,7 @@ private val SIZE = 128.dp
@OptIn(ExperimentalLayoutApi::class) @OptIn(ExperimentalLayoutApi::class)
@Composable @Composable
fun AttachmentRow( fun AttachmentRow(
attachments: List<TaskAttachment>, attachments: ImmutableSet<TaskAttachment>,
openAttachment: (TaskAttachment) -> Unit, openAttachment: (TaskAttachment) -> Unit,
deleteAttachment: (TaskAttachment) -> Unit, deleteAttachment: (TaskAttachment) -> Unit,
addAttachment: () -> Unit, addAttachment: () -> Unit,
@ -260,7 +262,7 @@ fun BoxScope.DeleteAttachment(
fun NoAttachments() { fun NoAttachments() {
TasksTheme { TasksTheme {
AttachmentRow( AttachmentRow(
attachments = emptyList(), attachments = persistentSetOf(),
openAttachment = {}, openAttachment = {},
deleteAttachment = {}, deleteAttachment = {},
addAttachment = {}, addAttachment = {},
@ -274,7 +276,7 @@ fun NoAttachments() {
fun AttachmentPreview() { fun AttachmentPreview() {
TasksTheme { TasksTheme {
AttachmentRow( AttachmentRow(
attachments = listOf( attachments = persistentSetOf(
TaskAttachment( TaskAttachment(
uri = "file://attachment.txt", uri = "file://attachment.txt",
name = "attachment.txt", name = "attachment.txt",

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

@ -7,6 +7,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentSetOf
import org.tasks.R import org.tasks.R
import org.tasks.compose.Chip import org.tasks.compose.Chip
import org.tasks.compose.ChipGroup import org.tasks.compose.ChipGroup
@ -19,7 +21,7 @@ import org.tasks.themes.TasksTheme
@Composable @Composable
fun TagsRow( fun TagsRow(
tags: List<TagData>, tags: ImmutableSet<TagData>,
colorProvider: (Int) -> Int, colorProvider: (Int) -> Int,
onClick: () -> Unit, onClick: () -> Unit,
onClear: (TagData) -> Unit, onClear: (TagData) -> Unit,
@ -56,7 +58,7 @@ fun TagsRow(
fun NoTags() { fun NoTags() {
TasksTheme { TasksTheme {
TagsRow( TagsRow(
tags = emptyList(), tags = persistentSetOf(),
colorProvider = { 0 }, colorProvider = { 0 },
onClick = {}, onClick = {},
onClear = {}, onClear = {},
@ -70,7 +72,7 @@ fun NoTags() {
fun SingleTag() { fun SingleTag() {
TasksTheme { TasksTheme {
TagsRow( TagsRow(
tags = listOf( tags = persistentSetOf(
TagData( TagData(
name = "Home", name = "Home",
icon = "home", icon = "home",
@ -89,7 +91,7 @@ fun SingleTag() {
fun BunchOfTags() { fun BunchOfTags() {
TasksTheme { TasksTheme {
TagsRow( TagsRow(
tags = listOf( tags = persistentSetOf(
TagData(name = "One"), TagData(name = "One"),
TagData(name = "Two"), TagData(name = "Two"),
TagData(name = "Three"), TagData(name = "Three"),
@ -108,7 +110,7 @@ fun BunchOfTags() {
fun TagWithReallyLongName() { fun TagWithReallyLongName() {
TasksTheme { TasksTheme {
TagsRow( TagsRow(
tags = listOf( tags = persistentSetOf(
TagData( TagData(
name = "This is a tag with a really really long name", name = "This is a tag with a really really long name",
icon = "home", 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.content.Context
import android.net.Uri import android.net.Uri
import org.tasks.data.entity.Place import org.tasks.data.entity.Place
import org.tasks.extensions.Context.findActivity
import org.tasks.extensions.Context.openUri import org.tasks.extensions.Context.openUri
import org.tasks.location.MapPosition import org.tasks.location.MapPosition
fun Place.open(context: Context?) = 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 val Place.mapPosition: MapPosition
get() = MapPosition(latitude, longitude) get() = MapPosition(latitude, longitude)

@ -17,7 +17,6 @@ import android.widget.LinearLayout
import androidx.core.widget.addTextChangedListener import androidx.core.widget.addTextChangedListener
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import com.todoroo.andlib.utility.AndroidUtilities
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.tasks.R import org.tasks.R
import org.tasks.Strings.isNullOrEmpty import org.tasks.Strings.isNullOrEmpty
@ -71,7 +70,7 @@ class CommentBarFragment : Fragment() {
commentField.maxLines = Int.MAX_VALUE commentField.maxLines = Int.MAX_VALUE
if ( if (
preferences.getBoolean(R.string.p_show_task_edit_comments, false) && preferences.getBoolean(R.string.p_show_task_edit_comments, false) &&
viewModel.isWritable !viewModel.viewState.value.isReadOnly
) { ) {
commentBar.visibility = View.VISIBLE 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.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast.LENGTH_SHORT import android.widget.Toast.LENGTH_SHORT
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.todoroo.astrid.activity.TaskEditFragment import com.todoroo.astrid.activity.TaskEditFragment
@ -37,17 +38,18 @@ class CalendarControlSet : TaskEditControlFragment() {
} }
} }
if (!canAccessCalendars) { if (!canAccessCalendars) {
viewModel.selectedCalendar.value = null viewModel.setCalendar(null)
} }
} }
override fun bind(parent: ViewGroup?): View = override fun bind(parent: ViewGroup?): View =
(parent as ComposeView).apply { (parent as ComposeView).apply {
setContent { setContent {
val viewState = viewModel.viewState.collectAsStateWithLifecycle().value
CalendarRow( CalendarRow(
eventUri = viewModel.eventUri.collectAsStateWithLifecycle().value, eventUri = viewModel.eventUri.collectAsStateWithLifecycle().value,
selectedCalendar = viewModel.selectedCalendar.collectAsStateWithLifecycle().value?.let { selectedCalendar = remember (viewState.calendar) {
calendarProvider.getCalendar(it)?.name calendarProvider.getCalendar(viewState.calendar)?.name
}, },
onClick = { onClick = {
if (viewModel.eventUri.value.isNullOrBlank()) { if (viewModel.eventUri.value.isNullOrBlank()) {
@ -55,7 +57,7 @@ class CalendarControlSet : TaskEditControlFragment() {
.newCalendarPicker( .newCalendarPicker(
requireParentFragment(), requireParentFragment(),
TaskEditFragment.REQUEST_CODE_PICK_CALENDAR, TaskEditFragment.REQUEST_CODE_PICK_CALENDAR,
viewModel.selectedCalendar.value, viewState.calendar,
) )
.show( .show(
requireParentFragment().parentFragmentManager, requireParentFragment().parentFragmentManager,
@ -66,7 +68,7 @@ class CalendarControlSet : TaskEditControlFragment() {
} }
}, },
clear = { clear = {
viewModel.selectedCalendar.value = null viewModel.setCalendar(null)
viewModel.eventUri.value = null viewModel.eventUri.value = null
} }
) )

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

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

@ -7,19 +7,33 @@ import androidx.core.net.toUri
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import com.todoroo.astrid.activity.BeastModePreferences
import com.todoroo.astrid.activity.TaskEditFragment import com.todoroo.astrid.activity.TaskEditFragment
import com.todoroo.astrid.alarms.AlarmService import com.todoroo.astrid.alarms.AlarmService
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.files.FilesControlSet
import com.todoroo.astrid.gcal.GCalHelper import com.todoroo.astrid.gcal.GCalHelper
import com.todoroo.astrid.repeats.RepeatControlSet
import com.todoroo.astrid.service.TaskCompleter import com.todoroo.astrid.service.TaskCompleter
import com.todoroo.astrid.service.TaskCreator.Companion.getDefaultAlarms import com.todoroo.astrid.service.TaskCreator.Companion.getDefaultAlarms
import com.todoroo.astrid.service.TaskDeleter import com.todoroo.astrid.service.TaskDeleter
import com.todoroo.astrid.service.TaskMover 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.timers.TimerPlugin
import com.todoroo.astrid.ui.ReminderControlSet
import com.todoroo.astrid.ui.StartDateControlSet
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext 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.NonCancellable
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
@ -94,30 +108,105 @@ class TaskEditViewModel @Inject constructor(
private val alarmDao: AlarmDao, private val alarmDao: AlarmDao,
private val taskAttachmentDao: TaskAttachmentDao, private val taskAttachmentDao: TaskAttachmentDao,
) : ViewModel() { ) : 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 val resources = context.resources
private var cleared = false private var cleared = false
val task: Task = savedStateHandle[TaskEditFragment.EXTRA_TASK]!! private val task: Task = savedStateHandle.get<Task>(TaskEditFragment.EXTRA_TASK)
?.let { it.copy(notes = it.notes?.stripCarriageReturns()) }
val isNew = task.isNew ?: 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) var eventUri = MutableStateFlow(task.calendarURI)
val timerStarted = MutableStateFlow(task.timerStart) val timerStarted = MutableStateFlow(task.timerStart)
val estimatedSeconds = MutableStateFlow(task.estimatedSeconds) val estimatedSeconds = MutableStateFlow(task.estimatedSeconds)
val elapsedSeconds = MutableStateFlow(task.elapsedSeconds) val elapsedSeconds = MutableStateFlow(task.elapsedSeconds)
var newSubtasks = MutableStateFlow(emptyList<Task>())
val hasParent: Boolean
get() = task.parent > 0
val dueDate = MutableStateFlow(task.dueDate) val dueDate = MutableStateFlow(task.dueDate)
@ -131,10 +220,14 @@ class TaskEditViewModel @Inject constructor(
if (addedDueDate) { if (addedDueDate) {
val reminderFlags = preferences.defaultReminders val reminderFlags = preferences.defaultReminders
if (reminderFlags.isFlagSet(Task.NOTIFY_AT_DEADLINE)) { 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)) { 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() else -> value.startOfDay()
} }
if (addedStartDate && preferences.defaultReminders.isFlagSet(Task.NOTIFY_AT_START)) { if (addedStartDate && preferences.defaultReminders.isFlagSet(Task.NOTIFY_AT_START)) {
selectedAlarms.value = selectedAlarms.value.plusAlarm(whenStarted(task.id)) _viewState.update { state ->
} state.copy(alarms = state.alarms.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))
} }
} }
} else {
savedStateHandle[TaskEditFragment.EXTRA_ALARMS]!!
} }
var selectedAlarms = MutableStateFlow(originalAlarms)
var ringNonstop: Boolean = task.isNotifyModeNonstop var ringNonstop: Boolean = task.isNotifyModeNonstop
set(value) { set(value) {
field = value field = value
@ -211,28 +265,12 @@ class TaskEditViewModel @Inject constructor(
} }
} }
val isReadOnly = task.readOnly fun hasChanges(): Boolean {
val viewState = _viewState.value
val isWritable = !isReadOnly return originalState != viewState ||
(viewState.isNew && viewState.task.title?.isNotBlank() == true) || // text shared to tasks
fun hasChanges(): Boolean =
(task.title != title || (isNew && title?.isNotBlank() == true)) ||
task.isCompleted != completed.value ||
task.dueDate != dueDate.value || task.dueDate != dueDate.value ||
task.priority != priority.value ||
if (task.notes.isNullOrBlank()) {
!description.isNullOrBlank()
} else {
task.notes != description
} ||
task.hideUntil != startDate.value || 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()) { if (task.calendarURI.isNullOrBlank()) {
!eventUri.value.isNullOrBlank() !eventUri.value.isNullOrBlank()
} else { } else {
@ -240,58 +278,49 @@ class TaskEditViewModel @Inject constructor(
} || } ||
task.elapsedSeconds != elapsedSeconds.value || task.elapsedSeconds != elapsedSeconds.value ||
task.estimatedSeconds != estimatedSeconds.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 { getRingFlags() != when {
task.isNotifyModeFive -> NOTIFY_MODE_FIVE task.isNotifyModeFive -> NOTIFY_MODE_FIVE
task.isNotifyModeNonstop -> NOTIFY_MODE_NONSTOP task.isNotifyModeNonstop -> NOTIFY_MODE_NONSTOP
else -> 0 else -> 0
} || }
originalAlarms.toHashSet() != selectedAlarms.value.toHashSet() }
@MainThread @MainThread
suspend fun save(remove: Boolean = true): Boolean = withContext(NonCancellable) { suspend fun save(remove: Boolean = true): Boolean = withContext(NonCancellable) {
if (cleared) { if (cleared) {
return@withContext false return@withContext false
} }
if (!hasChanges() || isReadOnly) { if (!hasChanges() || viewState.value.isReadOnly) {
discard(remove) discard(remove)
return@withContext false return@withContext false
} }
clear(remove) 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.dueDate = dueDate.value
task.priority = priority.value task.priority = viewState.task.priority
task.notes = description task.notes = viewState.task.notes
task.hideUntil = startDate.value task.hideUntil = startDate.value
task.recurrence = recurrence.value task.recurrence = viewState.task.recurrence
task.repeatFrom = if (repeatAfterCompletion.value) { task.repeatFrom = viewState.task.repeatFrom
Task.RepeatFrom.COMPLETION_DATE
} else {
Task.RepeatFrom.DUE_DATE
}
task.elapsedSeconds = elapsedSeconds.value task.elapsedSeconds = elapsedSeconds.value
task.estimatedSeconds = estimatedSeconds.value task.estimatedSeconds = estimatedSeconds.value
task.ringFlags = getRingFlags() task.ringFlags = getRingFlags()
applyCalendarChanges() applyCalendarChanges()
if (isNew) { if (isNew) {
taskDao.createNew(task) taskDao.createNew(task)
} }
val selectedLocation = _viewState.value.location
if ((isNew && selectedLocation.value != null) || originalLocation != selectedLocation.value) { if ((isNew && selectedLocation != null) || originalState.location != selectedLocation) {
originalLocation?.let { location -> originalState.location?.let { location ->
if (location.geofence.id > 0) { if (location.geofence.id > 0) {
locationDao.delete(location.geofence) locationDao.delete(location.geofence)
geofenceApi.update(location.place) geofenceApi.update(location.place)
} }
} }
selectedLocation.value?.let { location -> selectedLocation?.let { location ->
val place = location.place val place = location.place
locationDao.insert( locationDao.insert(
location.geofence.copy( location.geofence.copy(
@ -305,36 +334,45 @@ class TaskEditViewModel @Inject constructor(
task.putTransitory(FORCE_MICROSOFT_SYNC, true) task.putTransitory(FORCE_MICROSOFT_SYNC, true)
task.modificationDate = currentTimeMillis() task.modificationDate = currentTimeMillis()
} }
val selectedTags = _viewState.value.tags
if ((isNew && selectedTags.value.isNotEmpty()) || originalTags.toHashSet() != selectedTags.value.toHashSet()) { if ((isNew && selectedTags.isNotEmpty()) || originalState.tags.toHashSet() != selectedTags.toHashSet()) {
tagDao.applyTags(task, tagDataDao, selectedTags.value) tagDao.applyTags(task, tagDataDao, selectedTags)
task.putTransitory(FORCE_CALDAV_SYNC, true)
task.modificationDate = currentTimeMillis() task.modificationDate = currentTimeMillis()
} }
if (!task.hasStartDate()) { 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()) { 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 ( if (
selectedAlarms.value.toHashSet() != originalAlarms.toHashSet() || (isNew && _viewState.value.alarms.isNotEmpty()) ||
(isNew && selectedAlarms.value.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.putTransitory(FORCE_CALDAV_SYNC, true)
task.modificationDate = currentTimeMillis() task.modificationDate = currentTimeMillis()
} }
taskDao.save(task, null) taskDao.save(task, null)
val selectedList = _viewState.value.list
if (isNew || originalList != selectedList.value) { if (isNew || originalState.list != selectedList) {
task.parent = 0 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)) { if (Strings.isNullOrEmpty(subtask.title)) {
continue continue
} }
@ -344,7 +382,7 @@ class TaskEditViewModel @Inject constructor(
taskDao.createNew(subtask) taskDao.createNew(subtask)
alarmDao.insert(subtask.getDefaultAlarms()) alarmDao.insert(subtask.getDefaultAlarms())
firebase?.addTask("subtasks") firebase?.addTask("subtasks")
val filter = selectedList.value val filter = selectedList
when { when {
filter.isGoogleTasks -> { filter.isGoogleTasks -> {
val googleTask = CaldavTask( val googleTask = CaldavTask(
@ -363,7 +401,7 @@ class TaskEditViewModel @Inject constructor(
else -> { else -> {
val caldavTask = CaldavTask( val caldavTask = CaldavTask(
task = subtask.id, task = subtask.id,
calendar = filter.uuid, calendar = selectedList.uuid,
) )
subtask.parent = task.id subtask.parent = task.id
caldavTask.remoteParent = caldavDao.getRemoteIdForTask(task.id) caldavTask.remoteParent = caldavDao.getRemoteIdForTask(task.id)
@ -377,16 +415,13 @@ class TaskEditViewModel @Inject constructor(
} }
} }
if ( if (originalState.attachments != _viewState.value.attachments) {
this@TaskEditViewModel::originalAttachments.isInitialized && originalState.attachments
selectedAttachments.value.toHashSet() != originalAttachments.toHashSet() .minus(_viewState.value.attachments)
) {
originalAttachments
.minus(selectedAttachments.value.toSet())
.map { it.remoteId } .map { it.remoteId }
.let { taskAttachmentDao.delete(task.id, it) } .let { taskAttachmentDao.delete(task.id, it) }
selectedAttachments.value _viewState.value.attachments
.minus(originalAttachments.toSet()) .minus(originalState.attachments)
.map { .map {
Attachment( Attachment(
task = task.id, task = task.id,
@ -397,9 +432,9 @@ class TaskEditViewModel @Inject constructor(
.let { taskAttachmentDao.insert(it) } .let { taskAttachmentDao.insert(it) }
} }
if (task.isCompleted != completed.value) { if (task.isCompleted != _viewState.value.isCompleted) {
taskCompleter.setComplete(task, completed.value) taskCompleter.setComplete(task, _viewState.value.isCompleted)
if (task.isCompleted) { if (_viewState.value.isCompleted) {
firebase?.completeTask("edit_screen_v2") firebase?.completeTask("edit_screen_v2")
} }
} }
@ -424,7 +459,7 @@ class TaskEditViewModel @Inject constructor(
if (!task.hasDueDate()) { if (!task.hasDueDate()) {
return return
} }
selectedCalendar.value?.let { _viewState.value.calendar?.let {
try { try {
task.calendarURI = gCalHelper.createTaskEvent(task, it)?.toString() task.calendarURI = gCalHelper.createTaskEvent(task, it)?.toString()
} catch (e: Exception) { } catch (e: Exception) {
@ -445,11 +480,11 @@ class TaskEditViewModel @Inject constructor(
} }
suspend fun discard(remove: Boolean = true) { suspend fun discard(remove: Boolean = true) {
if (isNew) { if (_viewState.value.isNew) {
timerPlugin.stopTimer(task) timerPlugin.stopTimer(task)
originalAttachments.plus(selectedAttachments.value).toSet().takeIf { it.isNotEmpty() } (originalState.attachments + _viewState.value.attachments)
?.onEach { FileHelper.delete(context, it.uri.toUri()) } .onEach { attachment -> FileHelper.delete(context, attachment.uri.toUri()) }
?.let { taskAttachmentDao.delete(it.toList()) } .let { taskAttachmentDao.delete(it.toList()) }
} }
clear(remove) clear(remove)
} }
@ -474,14 +509,14 @@ class TaskEditViewModel @Inject constructor(
} }
fun removeAlarm(alarm: Alarm) { fun removeAlarm(alarm: Alarm) {
selectedAlarms.update { it.minus(alarm) } _viewState.update { state ->
state.copy(alarms = state.alarms.minus(alarm).toPersistentSet())
}
} }
fun addAlarm(alarm: Alarm) { fun addAlarm(alarm: Alarm) {
with (selectedAlarms) { _viewState.update { state ->
if (value.none { it.same(alarm) }) { state.copy(alarms = state.alarms.plusAlarm(alarm))
value = value.plus(alarm)
}
} }
} }
@ -502,26 +537,126 @@ class TaskEditViewModel @Inject constructor(
} }
fun hideBeastModeHint(click: Boolean) { fun hideBeastModeHint(click: Boolean) {
showBeastModeHint.value = false _viewState.update {
it.copy(showBeastModeHint = false)
}
preferences.shownBeastModeHint = true preferences.shownBeastModeHint = true
firebase?.logEvent(R.string.event_banner_beast, R.string.param_click to click) 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 { init {
_originalState = _viewState.value.copy()
viewModelScope.launch { viewModelScope.launch {
taskAttachmentDao.getAttachments(task.id).let { attachments -> taskAttachmentDao.getAttachments(task.id).toPersistentSet().let { attachments ->
selectedAttachments.update { attachments } _originalState = _originalState.copy(attachments = attachments)
originalAttachments = attachments _viewState.value = _viewState.value.copy(attachments = attachments)
} }
} }
} }
companion object { companion object {
// one spark tasks for windows adds these
fun String?.stripCarriageReturns(): String? = this?.replace("\\r\\n?".toRegex(), "\n") fun String?.stripCarriageReturns(): String? = this?.replace("\\r\\n?".toRegex(), "\n")
private fun Int.isFlagSet(flag: Int): Boolean = this and flag > 0 private fun Int.isFlagSet(flag: Int): Boolean = this and flag > 0
private fun List<Alarm>.plusAlarm(alarm: Alarm): List<Alarm> = private fun ImmutableSet<Alarm>.plusAlarm(alarm: Alarm): ImmutableSet<Alarm> =
if (any { it.same(alarm) }) this else this + 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> <item>@string/TEA_ctrl_hide_section_pref</item>
</string-array> </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_when_pref">TEA_ctrl_when_pref</string>
<string name="TEA_ctrl_repeat_pref">TEA_ctrl_repeat_pref</string> <string name="TEA_ctrl_repeat_pref">TEA_ctrl_repeat_pref</string>
<string name="TEA_ctrl_importance_pref">TEA_ctrl_importance_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 @Delete
abstract suspend fun delete(tags: List<Tag>) 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 { database.withTransaction {
val taskId = task.id val taskId = task.id
val existing = HashSet(tagDataDao.getTagDataForTask(taskId)) val existing = HashSet(tagDataDao.getTagDataForTask(taskId))
val selected = HashSet<TagData>(current) val selected = current.toMutableSet()
val added = selected subtract existing val added = selected subtract existing
val removed = existing subtract selected val removed = existing subtract selected
deleteTags(taskId, removed.map { td -> td.remoteId!! }) 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 */ /** Checks whether this due date has a due time or only a date */
fun hasDueTime(): Boolean = hasDueTime(dueDate) fun hasDueTime(): Boolean = hasDueTime(dueDate)
fun repeatAfterCompletion(): Boolean = repeatFrom == RepeatFrom.COMPLETION_DATE
fun setDueDateAdjustingHideUntil(newDueDate: Long) { fun setDueDateAdjustingHideUntil(newDueDate: Long) {
if (dueDate > 0) { if (dueDate > 0) {
if (hideUntil > 0) { if (hideUntil > 0) {
@ -283,6 +281,7 @@ data class Task @OptIn(ExperimentalSerializationApi::class) constructor(
} }
} }
@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE)
@Retention(AnnotationRetention.SOURCE) @Retention(AnnotationRetention.SOURCE)
@IntDef(RepeatFrom.DUE_DATE, RepeatFrom.COMPLETION_DATE) @IntDef(RepeatFrom.DUE_DATE, RepeatFrom.COMPLETION_DATE)
annotation class RepeatFrom { annotation class RepeatFrom {

Loading…
Cancel
Save