Refactoring TaskEditFragment

pull/3363/head
Alex Baker 10 months ago
parent ea08976c06
commit 2d8df0bb67

@ -43,17 +43,14 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.IntentCompat.getParcelableExtra import androidx.core.content.IntentCompat.getParcelableExtra
import androidx.fragment.compose.AndroidFragment import androidx.fragment.compose.AndroidFragment
import androidx.fragment.compose.rememberFragmentState import androidx.fragment.compose.rememberFragmentState
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.todoroo.andlib.utility.AndroidUtilities.atLeastR import com.todoroo.andlib.utility.AndroidUtilities.atLeastR
import com.todoroo.astrid.activity.TaskEditFragment.Companion.EXTRA_TASK import com.todoroo.astrid.activity.TaskEditFragment.Companion.EXTRA_TASK
import com.todoroo.astrid.activity.TaskListFragment.Companion.EXTRA_FILTER import com.todoroo.astrid.activity.TaskListFragment.Companion.EXTRA_FILTER
@ -151,7 +148,6 @@ class MainActivity : AppCompatActivity() {
setContent { setContent {
TasksTheme(theme = theme.themeBase.index) { TasksTheme(theme = theme.themeBase.index) {
val configuration = LocalConfiguration.current
val navigator = rememberListDetailPaneScaffoldNavigator( val navigator = rememberListDetailPaneScaffoldNavigator(
calculatePaneScaffoldDirective( calculatePaneScaffoldDirective(
windowAdaptiveInfo = currentWindowAdaptiveInfo(), windowAdaptiveInfo = currentWindowAdaptiveInfo(),

@ -4,75 +4,34 @@ import android.app.Activity
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier import androidx.compose.runtime.remember
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.core.os.BundleCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.fragment.compose.AndroidFragment
import androidx.fragment.compose.content import androidx.fragment.compose.content
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.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.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.tasks.R import org.tasks.R
import org.tasks.calendars.CalendarPicker import org.tasks.calendars.CalendarPicker
import org.tasks.compose.FilterSelectionActivity.Companion.launch
import org.tasks.compose.FilterSelectionActivity.Companion.registerForListPickerResult
import org.tasks.compose.edit.DescriptionRow
import org.tasks.compose.edit.DueDateRow
import org.tasks.compose.edit.InfoRow
import org.tasks.compose.edit.ListRow
import org.tasks.compose.edit.PriorityRow
import org.tasks.compose.edit.TaskEditScreen import org.tasks.compose.edit.TaskEditScreen
import org.tasks.compose.edit.TitleRow
import org.tasks.data.dao.UserActivityDao import org.tasks.data.dao.UserActivityDao
import org.tasks.data.entity.Task
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.dialogs.DateTimePicker import org.tasks.dialogs.DateTimePicker
import org.tasks.dialogs.DialogBuilder 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.hideKeyboard import org.tasks.extensions.hideKeyboard
import org.tasks.kmp.org.tasks.time.DateStyle
import org.tasks.kmp.org.tasks.time.getRelativeDateTime
import org.tasks.markdown.MarkdownProvider import org.tasks.markdown.MarkdownProvider
import org.tasks.notifications.NotificationManager import org.tasks.notifications.NotificationManager
import org.tasks.play.PlayServices import org.tasks.play.PlayServices
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.themes.TasksTheme import org.tasks.themes.TasksTheme
import org.tasks.themes.Theme import org.tasks.themes.Theme
import org.tasks.ui.CalendarControlSet
import org.tasks.ui.ChipProvider import org.tasks.ui.ChipProvider
import org.tasks.ui.LocationControlSet
import org.tasks.ui.SubtaskControlSet
import org.tasks.ui.TaskEditEvent
import org.tasks.ui.TaskEditEventBus
import org.tasks.ui.TaskEditViewModel import org.tasks.ui.TaskEditViewModel
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
@ -83,8 +42,6 @@ class TaskEditFragment : Fragment() {
@Inject lateinit var dialogBuilder: DialogBuilder @Inject lateinit var dialogBuilder: DialogBuilder
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
@Inject lateinit var linkify: Linkify @Inject lateinit var linkify: Linkify
@Inject lateinit var markdownProvider: MarkdownProvider
@Inject lateinit var taskEditEventBus: TaskEditEventBus
@Inject lateinit var locale: Locale @Inject lateinit var locale: Locale
@Inject lateinit var chipProvider: ChipProvider @Inject lateinit var chipProvider: ChipProvider
@Inject lateinit var playServices: PlayServices @Inject lateinit var playServices: PlayServices
@ -92,27 +49,12 @@ class TaskEditFragment : Fragment() {
private val editViewModel: TaskEditViewModel by viewModels() private val editViewModel: TaskEditViewModel by viewModels()
private val mainViewModel: MainActivityViewModel by activityViewModels() private val mainViewModel: MainActivityViewModel by activityViewModels()
private val beastMode =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
activity?.recreate()
}
private val listPickerLauncher = registerForListPickerResult { filter ->
editViewModel.setList(filter)
}
val task: Task?
get() = BundleCompat.getParcelable(requireArguments(), EXTRA_TASK, Task::class.java)
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
) = content { ) = content {
LaunchedEffect(Unit) {
if (atLeastOreoMR1()) {
activity?.setShowWhenLocked(preferences.showEditScreenWithoutUnlock)
}
}
TasksTheme(theme = theme.themeBase.index,) { TasksTheme(theme = theme.themeBase.index,) {
val viewState = editViewModel.viewState.collectAsStateWithLifecycle().value val viewState = editViewModel.viewState.collectAsStateWithLifecycle().value
LaunchedEffect(viewState.isNew) { LaunchedEffect(viewState.isNew) {
@ -120,27 +62,39 @@ class TaskEditFragment : Fragment() {
notificationManager.cancel(viewState.task.id) notificationManager.cancel(viewState.task.id)
} }
} }
val context = LocalContext.current
TaskEditScreen( TaskEditScreen(
editViewModel = editViewModel,
viewState = viewState, viewState = viewState,
comments = userActivityDao comments = userActivityDao
.watchComments(viewState.task.uuid) .watchComments(viewState.task.uuid)
.collectAsStateWithLifecycle(emptyList()) .collectAsStateWithLifecycle(emptyList())
.value, .value,
save = { lifecycleScope.launch { save() } }, save = { lifecycleScope.launch { save() } },
discard = { discardButtonClick() }, discard = {
onBackPressed = { activity?.hideKeyboard()
if (viewState.backButtonSavesTask) { if (editViewModel.hasChanges()) {
lifecycleScope.launch { dialogBuilder
save() .newDialog(R.string.discard_confirmation)
} .setPositiveButton(R.string.keep_editing, null)
.setNegativeButton(R.string.discard) { _, _ -> discard() }
.show()
} else { } else {
discardButtonClick() discard()
} }
}, },
delete = { deleteButtonClick() }, delete = {
openBeastModeSettings = { activity?.hideKeyboard()
editViewModel.hideBeastModeHint(click = true) dialogBuilder
beastMode.launch(Intent(context, BeastModePreferences::class.java)) .newDialog(R.string.DLG_delete_this_task_question)
.setPositiveButton(R.string.ok) { _, _ ->
lifecycleScope.launch {
editViewModel.delete()
mainViewModel.setTask(null)
}
}
.setNegativeButton(R.string.cancel, null)
.show()
}, },
dismissBeastMode = { editViewModel.hideBeastModeHint(click = false) }, dismissBeastMode = { editViewModel.hideBeastModeHint(click = false) },
deleteComment = { deleteComment = {
@ -148,86 +102,26 @@ class TaskEditFragment : Fragment() {
userActivityDao.delete(it) userActivityDao.delete(it)
} }
}, },
) { tag -> markdownProvider = remember { MarkdownProvider(context, preferences) },
val context = LocalContext.current
when (tag) {
TAG_TITLE ->
TitleRow(
viewState = viewState,
requestFocus = viewState.showKeyboard,
)
TAG_DUE_DATE -> DueDateRow()
TAG_PRIORITY ->
PriorityRow(
priority = viewState.task.priority,
onChangePriority = { editViewModel.setPriority(it) },
)
TAG_DESCRIPTION ->
DescriptionRow(
text = viewState.task.notes,
onChanged = { text -> editViewModel.setDescription(text.toString().trim { it <= ' ' }) },
linkify = if (viewState.linkify) linkify else null, linkify = if (viewState.linkify) linkify else null,
markdownProvider = markdownProvider, onClickDueDate = {
DateTimePicker
.newDateTimePicker(
target = this@TaskEditFragment,
rc = REQUEST_DATE,
current = editViewModel.dueDate.value,
autoClose = preferences.getBoolean(
R.string.p_auto_dismiss_datetime_edit_screen,
false
),
hideNoDate = viewState.task.isRecurring,
) )
.show(parentFragmentManager, FRAG_TAG_DATE_PICKER)
TAG_LIST -> },
ListRow(
list = viewState.list,
colorProvider = { chipProvider.getColor(it) }, colorProvider = { chipProvider.getColor(it) },
onClick = { locale = remember { locale },
listPickerLauncher.launch(
context = context,
selectedFilter = viewState.list,
listsOnly = true
) )
} }
)
TAG_CREATION ->
InfoRow(
creationDate = viewState.task.creationDate,
modificationDate = viewState.task.modificationDate,
completionDate = viewState.task.completionDate,
locale = locale,
)
CalendarControlSet.TAG -> AndroidFragment<CalendarControlSet>()
StartDateControlSet.TAG -> AndroidFragment<StartDateControlSet>()
ReminderControlSet.TAG -> AndroidFragment<ReminderControlSet>()
LocationControlSet.TAG -> AndroidFragment<LocationControlSet>()
FilesControlSet.TAG -> AndroidFragment<FilesControlSet>()
TimerControlSet.TAG -> AndroidFragment<TimerControlSet>()
TagsControlSet.TAG -> AndroidFragment<TagsControlSet>()
RepeatControlSet.TAG -> AndroidFragment<RepeatControlSet>()
SubtaskControlSet.TAG -> AndroidFragment<SubtaskControlSet>()
else -> throw IllegalArgumentException("Unknown row: $tag")
}
}
}
}
override fun onDestroyView() {
super.onDestroyView()
if (atLeastOreoMR1()) {
activity?.setShowWhenLocked(false)
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
taskEditEventBus
.onEach(this::process)
.launchIn(viewLifecycleOwner.lifecycleScope)
}
private suspend fun process(event: TaskEditEvent) {
when (event) {
is TaskEditEvent.Discard ->
if (event.id == editViewModel.viewState.value.task.id) {
editViewModel.discard()
}
}
} }
suspend fun save(remove: Boolean = true) { suspend fun save(remove: Boolean = true) {
@ -238,38 +132,11 @@ class TaskEditFragment : Fragment() {
activity?.let { playServices.requestReview(it) } activity?.let { playServices.requestReview(it) }
} }
private fun discardButtonClick() {
activity?.hideKeyboard()
if (editViewModel.hasChanges()) {
dialogBuilder
.newDialog(R.string.discard_confirmation)
.setPositiveButton(R.string.keep_editing, null)
.setNegativeButton(R.string.discard) { _, _ -> discard() }
.show()
} else {
discard()
}
}
private fun deleteButtonClick() {
activity?.hideKeyboard()
dialogBuilder
.newDialog(R.string.DLG_delete_this_task_question)
.setPositiveButton(R.string.ok) { _, _ -> delete() }
.setNegativeButton(R.string.cancel, null)
.show()
}
private fun discard() = lifecycleScope.launch { private fun discard() = lifecycleScope.launch {
editViewModel.discard() editViewModel.discard()
mainViewModel.setTask(null) mainViewModel.setTask(null)
} }
private fun delete() = lifecycleScope.launch {
editViewModel.delete()
mainViewModel.setTask(null)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) { when (requestCode) {
REQUEST_DATE -> { REQUEST_DATE -> {
@ -286,70 +153,6 @@ class TaskEditFragment : Fragment() {
} }
} }
@Composable
private fun TitleRow(
viewState: TaskEditViewModel.ViewState,
requestFocus: Boolean,
) {
TitleRow(
text = viewState.task.title,
onChanged = { text -> editViewModel.setTitle(text.toString().trim { it <= ' ' }) },
linkify = if (viewState.linkify) linkify else null,
markdownProvider = markdownProvider,
isCompleted = viewState.isCompleted,
isRecurring = viewState.task.isRecurring,
priority = viewState.task.priority,
onComplete = {
if (viewState.isCompleted) {
editViewModel.setComplete(false)
} else {
editViewModel.setComplete(true)
lifecycleScope.launch {
save()
}
}
},
requestFocus = requestFocus,
multiline = viewState.multilineTitle,
)
}
@Composable
private fun DueDateRow() {
val viewState = editViewModel.viewState.collectAsStateWithLifecycle().value
val dueDate = editViewModel.dueDate.collectAsStateWithLifecycle().value
val context = LocalContext.current
DueDateRow(
dueDate = if (dueDate == 0L) {
null
} else {
runBlocking {
getRelativeDateTime(
dueDate,
context.is24HourFormat,
DateStyle.FULL,
alwaysDisplayFullDate = preferences.alwaysDisplayFullDate
)
}
},
overdue = dueDate.isOverdue,
onClick = {
DateTimePicker
.newDateTimePicker(
target = this@TaskEditFragment,
rc = REQUEST_DATE,
current = editViewModel.dueDate.value,
autoClose = preferences.getBoolean(
R.string.p_auto_dismiss_datetime_edit_screen,
false
),
hideNoDate = viewState.task.isRecurring,
)
.show(parentFragmentManager, FRAG_TAG_DATE_PICKER)
}
)
}
companion object { companion object {
const val EXTRA_TASK = "extra_task" const val EXTRA_TASK = "extra_task"
@ -357,28 +160,5 @@ class TaskEditFragment : Fragment() {
private const val FRAG_TAG_DATE_PICKER = "frag_tag_date_picker" private const val FRAG_TAG_DATE_PICKER = "frag_tag_date_picker"
const val REQUEST_CODE_PICK_CALENDAR = 70 const val REQUEST_CODE_PICK_CALENDAR = 70
private const val REQUEST_DATE = 504 private const val REQUEST_DATE = 504
val Long.isOverdue: Boolean
get() = if (Task.hasDueTime(this)) {
newDateTime(this).isBeforeNow
} else {
newDateTime(this).endOfDay().isBeforeNow
}
fun Modifier.gesturesDisabled(disabled: Boolean = true) =
if (disabled) {
pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
awaitPointerEvent(pass = PointerEventPass.Initial)
.changes
.filter { it.position == it.previousPosition }
.forEach { it.consume() }
}
}
}
} else {
this
}
} }
} }

@ -6,7 +6,6 @@
package com.todoroo.astrid.activity package com.todoroo.astrid.activity
import android.Manifest import android.Manifest
import android.app.Activity
import android.app.Activity.RESULT_OK import android.app.Activity.RESULT_OK
import android.content.BroadcastReceiver import android.content.BroadcastReceiver
import android.content.Context import android.content.Context
@ -143,8 +142,6 @@ import org.tasks.themes.Theme
import org.tasks.themes.ThemeColor import org.tasks.themes.ThemeColor
import org.tasks.time.DateTimeUtils2.currentTimeMillis import org.tasks.time.DateTimeUtils2.currentTimeMillis
import org.tasks.ui.Banner import org.tasks.ui.Banner
import org.tasks.ui.TaskEditEvent
import org.tasks.ui.TaskEditEventBus
import org.tasks.ui.TaskListEvent import org.tasks.ui.TaskListEvent
import org.tasks.ui.TaskListEventBus import org.tasks.ui.TaskListEventBus
import org.tasks.ui.TaskListViewModel import org.tasks.ui.TaskListViewModel
@ -181,7 +178,6 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
@Inject lateinit var firebase: Firebase @Inject lateinit var firebase: Firebase
@Inject lateinit var repeatTaskHelper: RepeatTaskHelper @Inject lateinit var repeatTaskHelper: RepeatTaskHelper
@Inject lateinit var taskListEventBus: TaskListEventBus @Inject lateinit var taskListEventBus: TaskListEventBus
@Inject lateinit var taskEditEventBus: TaskEditEventBus
@Inject lateinit var database: Database @Inject lateinit var database: Database
@Inject lateinit var markdown: MarkdownProvider @Inject lateinit var markdown: MarkdownProvider
@Inject lateinit var theme: Theme @Inject lateinit var theme: Theme
@ -717,16 +713,9 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
} }
} }
private suspend fun onTaskDelete(task: Task) {
taskEditEventBus.emit(TaskEditEvent.Discard(task.id))
timerPlugin.stopTimer(task)
taskAdapter.onTaskDeleted(task)
loadTaskListContent()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) { when (requestCode) {
VOICE_RECOGNITION_REQUEST_CODE -> if (resultCode == Activity.RESULT_OK) { VOICE_RECOGNITION_REQUEST_CODE -> if (resultCode == RESULT_OK) {
lifecycleScope.launch { lifecycleScope.launch {
val match: List<String>? = data!!.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS) val match: List<String>? = data!!.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)
if (!match.isNullOrEmpty() && match[0].isNotEmpty()) { if (!match.isNullOrEmpty() && match[0].isNotEmpty()) {
@ -961,7 +950,14 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
val result = withContext(NonCancellable) { val result = withContext(NonCancellable) {
listViewModel.markDeleted(tasks) listViewModel.markDeleted(tasks)
} }
result.forEach { onTaskDelete(it) } result.forEach {
timerPlugin.stopTimer(it)
taskAdapter.onTaskDeleted(it)
}
loadTaskListContent()
if (tasks.contains(mainViewModel.state.value.task?.id)) {
mainViewModel.setTask(null)
}
makeSnackbar(R.string.delete_multiple_tasks_confirmation, result.size.toString())?.show() makeSnackbar(R.string.delete_multiple_tasks_confirmation, result.size.toString())?.show()
} }

@ -71,8 +71,6 @@ class FilesControlSet : TaskEditControlFragment() {
} }
} }
override fun controlId() = TAG
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == AddAttachmentDialog.REQUEST_CAMERA || requestCode == AddAttachmentDialog.REQUEST_AUDIO) { if (requestCode == AddAttachmentDialog.REQUEST_CAMERA || requestCode == AddAttachmentDialog.REQUEST_AUDIO) {
if (resultCode == Activity.RESULT_OK) { if (resultCode == Activity.RESULT_OK) {

@ -98,8 +98,6 @@ class RepeatControlSet : TaskEditControlFragment() {
} }
} }
override fun controlId() = TAG
companion object { companion object {
val TAG = R.string.TEA_ctrl_repeat_pref val TAG = R.string.TEA_ctrl_repeat_pref
private const val FRAG_TAG_BASIC_RECURRENCE = "frag_tag_basic_recurrence" private const val FRAG_TAG_BASIC_RECURRENCE = "frag_tag_basic_recurrence"

@ -36,7 +36,6 @@ class TaskDeleter @Inject constructor(
private val userActivityDao: UserActivityDao, private val userActivityDao: UserActivityDao,
private val locationDao: LocationDao, private val locationDao: LocationDao,
) { ) {
suspend fun markDeleted(item: Task) = markDeleted(listOf(item.id)) suspend fun markDeleted(item: Task) = markDeleted(listOf(item.id))
suspend fun markDeleted(taskIds: List<Long>): List<Task> = withContext(NonCancellable) { suspend fun markDeleted(taskIds: List<Long>): List<Task> = withContext(NonCancellable) {
@ -107,4 +106,6 @@ class TaskDeleter @Inject constructor(
} }
} }
} }
fun isDeleted(task: Long): Boolean = deletionDao.isDeleted(task)
} }

@ -42,8 +42,6 @@ class TagsControlSet : TaskEditControlFragment() {
} }
} }
override fun controlId() = TAG
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) {

@ -99,8 +99,6 @@ class TimerControlSet : TaskEditControlFragment() {
} }
} }
override fun controlId() = TAG
private fun timerActive() = viewModel.timerStarted.value > 0 private fun timerActive() = viewModel.timerStarted.value > 0
private suspend fun stopTimer(): Task { private suspend fun stopTimer(): Task {

@ -128,8 +128,6 @@ class ReminderControlSet : TaskEditControlFragment() {
} }
} }
override fun controlId() = TAG
companion object { companion object {
val TAG = R.string.TEA_ctrl_reminders_pref val TAG = R.string.TEA_ctrl_reminders_pref
private const val EXTRA_REPLACE = "extra_replace" private const val EXTRA_REPLACE = "extra_replace"

@ -89,8 +89,6 @@ class StartDateControlSet : TaskEditControlFragment() {
} }
} }
override fun controlId() = TAG
@Deprecated("Deprecated in Java") @Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_START_DATE) { if (requestCode == REQUEST_START_DATE) {

@ -11,13 +11,47 @@ import androidx.compose.ui.res.colorResource
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.coroutines.runBlocking
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.date.DateTimeUtils.newDateTime
import org.tasks.kmp.org.tasks.time.DateStyle
import org.tasks.kmp.org.tasks.time.getRelativeDateTime
import org.tasks.themes.TasksTheme import org.tasks.themes.TasksTheme
@Composable @Composable
fun DueDateRow( fun DueDateRow(
dueDate: Long,
is24HourFormat: Boolean,
alwaysDisplayFullDate: Boolean,
onClick: () -> Unit,
) {
DueDateRow(
dueDate = if (dueDate == 0L) {
null
} else {
runBlocking {
getRelativeDateTime(
dueDate,
is24HourFormat,
DateStyle.FULL,
alwaysDisplayFullDate = alwaysDisplayFullDate
)
}
},
overdue = if (Task.hasDueTime(dueDate)) {
newDateTime(dueDate).isBeforeNow
} else {
newDateTime(dueDate).endOfDay().isBeforeNow
},
onClick = onClick,
)
}
@Composable
private fun DueDateRow(
dueDate: String?, dueDate: String?,
overdue: Boolean, overdue: Boolean,
onClick: () -> Unit, onClick: () -> Unit,

@ -1,7 +1,10 @@
package org.tasks.compose.edit package org.tasks.compose.edit
import android.content.Intent
import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -22,36 +25,90 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
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.core.content.IntentCompat.getParcelableExtra
import androidx.fragment.compose.AndroidFragment import androidx.fragment.compose.AndroidFragment
import com.todoroo.astrid.activity.TaskEditFragment.Companion.gesturesDisabled import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.todoroo.andlib.utility.AndroidUtilities.atLeastOreoMR1
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 kotlinx.coroutines.launch
import org.tasks.R import org.tasks.R
import org.tasks.compose.BeastModeBanner import org.tasks.compose.BeastModeBanner
import org.tasks.compose.FilterSelectionActivity.Companion.EXTRA_FILTER
import org.tasks.compose.FilterSelectionActivity.Companion.launch
import org.tasks.data.entity.UserActivity import org.tasks.data.entity.UserActivity
import org.tasks.dialogs.Linkify
import org.tasks.extensions.Context.findActivity
import org.tasks.extensions.Context.is24HourFormat
import org.tasks.files.FileHelper import org.tasks.files.FileHelper
import org.tasks.filters.CaldavFilter
import org.tasks.fragments.CommentBarFragment import org.tasks.fragments.CommentBarFragment
import org.tasks.kmp.org.tasks.extensions.gesturesDisabled
import org.tasks.kmp.org.tasks.taskedit.TaskEditViewState
import org.tasks.markdown.MarkdownProvider
import org.tasks.themes.TasksTheme import org.tasks.themes.TasksTheme
import org.tasks.ui.CalendarControlSet
import org.tasks.ui.LocationControlSet
import org.tasks.ui.SubtaskControlSet
import org.tasks.ui.TaskEditViewModel import org.tasks.ui.TaskEditViewModel
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 org.tasks.utility.copyToClipboard import org.tasks.utility.copyToClipboard
import timber.log.Timber import timber.log.Timber
import java.util.Locale
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun TaskEditScreen( fun TaskEditScreen(
viewState: TaskEditViewModel.ViewState, editViewModel: TaskEditViewModel,
viewState: TaskEditViewState,
comments: List<UserActivity>, comments: List<UserActivity>,
save: () -> Unit, save: () -> Unit,
discard: () -> Unit, discard: () -> Unit,
onBackPressed: () -> Unit,
delete: () -> Unit, delete: () -> Unit,
openBeastModeSettings: () -> Unit,
dismissBeastMode: () -> Unit, dismissBeastMode: () -> Unit,
deleteComment: (UserActivity) -> Unit, deleteComment: (UserActivity) -> Unit,
content: @Composable (Int) -> Unit, onClickDueDate: () -> Unit,
markdownProvider: MarkdownProvider,
linkify: Linkify?,
locale: Locale,
colorProvider: (Int) -> Int,
) { ) {
val context = LocalContext.current
DisposableEffect(Unit) {
val activity = context.findActivity()
if (atLeastOreoMR1() && viewState.showEditScreenWithoutUnlock) {
activity?.setShowWhenLocked(true)
}
onDispose {
if (atLeastOreoMR1()) {
activity?.setShowWhenLocked(false)
}
}
}
val onBackPressed = {
if (viewState.backButtonSavesTask) {
save()
} else {
discard()
}
}
BackHandler { BackHandler {
Timber.d("onBackPressed") Timber.d("onBackPressed")
onBackPressed() onBackPressed()
@ -118,12 +175,98 @@ fun TaskEditScreen(
.fillMaxSize() .fillMaxSize()
.verticalScroll(rememberScrollState()), .verticalScroll(rememberScrollState()),
) { ) {
val scope = rememberCoroutineScope()
viewState.displayOrder.forEach { tag -> viewState.displayOrder.forEach { tag ->
content(tag) when (tag) {
TAG_TITLE -> {
TitleRow(
text = viewState.task.title,
onChanged = { text: CharSequence? ->
editViewModel.setTitle(text.toString().trim { it <= ' ' })
},
linkify = linkify,
markdownProvider = markdownProvider,
isCompleted = viewState.isCompleted,
isRecurring = viewState.task.isRecurring,
priority = viewState.task.priority,
onComplete = {
if (viewState.isCompleted) {
editViewModel.setComplete(false)
} else {
editViewModel.setComplete(true)
scope.launch {
save()
}
}
},
requestFocus = viewState.showKeyboard,
multiline = viewState.multilineTitle,
)
}
TAG_DUE_DATE -> DueDateRow(
dueDate = editViewModel.dueDate.collectAsStateWithLifecycle().value,
is24HourFormat = context.is24HourFormat,
alwaysDisplayFullDate = viewState.alwaysDisplayFullDate,
onClick = onClickDueDate,
)
TAG_PRIORITY ->
PriorityRow(
priority = viewState.task.priority,
onChangePriority = { editViewModel.setPriority(it) },
)
TAG_DESCRIPTION ->
DescriptionRow(
text = viewState.task.notes,
onChanged = { text -> editViewModel.setDescription(text.toString().trim { it <= ' ' }) },
linkify = if (viewState.linkify) linkify else null,
markdownProvider = markdownProvider,
)
TAG_LIST -> {
val listPickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { it ->
it.data
?.let { getParcelableExtra(it, EXTRA_FILTER, CaldavFilter::class.java) }
?.let { editViewModel.setList(it) }
}
ListRow(
list = viewState.list,
colorProvider = colorProvider,
onClick = {
listPickerLauncher.launch(
context = context,
selectedFilter = viewState.list,
listsOnly = true
)
}
)
}
TAG_CREATION ->
InfoRow(
creationDate = viewState.task.creationDate,
modificationDate = viewState.task.modificationDate,
completionDate = viewState.task.completionDate,
locale = locale,
)
CalendarControlSet.TAG -> AndroidFragment<CalendarControlSet>()
StartDateControlSet.TAG -> AndroidFragment<StartDateControlSet>()
ReminderControlSet.TAG -> AndroidFragment<ReminderControlSet>()
LocationControlSet.TAG -> AndroidFragment<LocationControlSet>()
FilesControlSet.TAG -> AndroidFragment<FilesControlSet>()
TimerControlSet.TAG -> AndroidFragment<TimerControlSet>()
TagsControlSet.TAG -> AndroidFragment<TagsControlSet>()
RepeatControlSet.TAG -> AndroidFragment<RepeatControlSet>()
SubtaskControlSet.TAG -> AndroidFragment<SubtaskControlSet>()
else -> throw IllegalArgumentException("Unknown row: $tag")
}
HorizontalDivider() HorizontalDivider()
} }
if (viewState.showComments) { if (viewState.showComments) {
val context = LocalContext.current
CommentsRow( CommentsRow(
comments = comments, comments = comments,
copyCommentToClipboard = { copyToClipboard(context, R.string.comment, it) }, copyCommentToClipboard = { copyToClipboard(context, R.string.comment, it) },
@ -131,9 +274,17 @@ fun TaskEditScreen(
openImage = { FileHelper.startActionView(context, it) }, openImage = { FileHelper.startActionView(context, it) },
) )
} }
val beastMode = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) {
context.findActivity()?.recreate()
}
BeastModeBanner( BeastModeBanner(
visible = viewState.showBeastModeHint, visible = viewState.showBeastModeHint,
showSettings = openBeastModeSettings, showSettings = {
editViewModel.hideBeastModeHint(click = true)
beastMode.launch(Intent(context, BeastModePreferences::class.java))
},
dismiss = dismissBeastMode, dismiss = dismissBeastMode,
) )
} }

@ -7,7 +7,6 @@ import dagger.hilt.android.components.ActivityRetainedComponent
import dagger.hilt.android.scopes.ActivityRetainedScoped import dagger.hilt.android.scopes.ActivityRetainedScoped
import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import org.tasks.ui.TaskEditEventBus
import org.tasks.ui.TaskListEventBus import org.tasks.ui.TaskListEventBus
@Module @Module
@ -17,10 +16,6 @@ class ActivityRetainedModule {
@ActivityRetainedScoped @ActivityRetainedScoped
fun getTaskListBus(): TaskListEventBus = makeFlow() fun getTaskListBus(): TaskListEventBus = makeFlow()
@Provides
@ActivityRetainedScoped
fun getTaskEditBus(): TaskEditEventBus = makeFlow()
private fun <T> makeFlow() = MutableSharedFlow<T>( private fun <T> makeFlow() = MutableSharedFlow<T>(
extraBufferCapacity = 1, extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST onBufferOverflow = BufferOverflow.DROP_OLDEST

@ -75,8 +75,6 @@ class CalendarControlSet : TaskEditControlFragment() {
} }
} }
override fun controlId() = TAG
private fun openCalendarEvent() { private fun openCalendarEvent() {
val cr = activity.contentResolver val cr = activity.contentResolver
val uri = Uri.parse(viewModel.eventUri.value) val uri = Uri.parse(viewModel.eventUri.value)

@ -105,8 +105,6 @@ class LocationControlSet : TaskEditControlFragment() {
} }
} }
override fun controlId() = TAG
private fun openWebsite() { private fun openWebsite() {
viewModel.viewState.value.location?.let { context?.openUri(it.url) } viewModel.viewState.value.location?.let { context?.openUri(it.url) }
} }

@ -89,8 +89,6 @@ class SubtaskControlSet : TaskEditControlFragment() {
} }
} }
override fun controlId() = TAG
private fun openSubtask(task: Task) = lifecycleScope.launch { private fun openSubtask(task: Task) = lifecycleScope.launch {
mainViewModel.setTask(task) mainViewModel.setTask(task)
} }

@ -26,6 +26,4 @@ abstract class TaskEditControlFragment : Fragment() {
abstract fun bind(parent: ViewGroup?): View abstract fun bind(parent: ViewGroup?): View
protected open fun createView(savedInstanceState: Bundle?) {} protected open fun createView(savedInstanceState: Bundle?) {}
abstract fun controlId(): Int
} }

@ -1,9 +0,0 @@
package org.tasks.ui
import kotlinx.coroutines.flow.MutableSharedFlow
typealias TaskEditEventBus = MutableSharedFlow<TaskEditEvent>
sealed interface TaskEditEvent {
data class Discard(val id: Long) : TaskEditEvent
}

@ -25,9 +25,7 @@ import com.todoroo.astrid.ui.ReminderControlSet
import com.todoroo.astrid.ui.StartDateControlSet 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.ImmutableSet
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.persistentSetOf
import kotlinx.collections.immutable.toPersistentList import kotlinx.collections.immutable.toPersistentList
import kotlinx.collections.immutable.toPersistentSet import kotlinx.collections.immutable.toPersistentSet
@ -77,6 +75,7 @@ import org.tasks.data.setPicture
import org.tasks.date.DateTimeUtils.toDateTime import org.tasks.date.DateTimeUtils.toDateTime
import org.tasks.files.FileHelper import org.tasks.files.FileHelper
import org.tasks.filters.CaldavFilter import org.tasks.filters.CaldavFilter
import org.tasks.kmp.org.tasks.taskedit.TaskEditViewState
import org.tasks.location.GeofenceApi import org.tasks.location.GeofenceApi
import org.tasks.preferences.DefaultFilterProvider import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.PermissionChecker import org.tasks.preferences.PermissionChecker
@ -113,33 +112,6 @@ class TaskEditViewModel @Inject constructor(
private val taskAttachmentDao: TaskAttachmentDao, private val taskAttachmentDao: TaskAttachmentDao,
private val defaultFilterProvider: DefaultFilterProvider, private val defaultFilterProvider: DefaultFilterProvider,
) : 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 multilineTitle: Boolean,
) {
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
@ -149,7 +121,7 @@ class TaskEditViewModel @Inject constructor(
?: throw IllegalArgumentException("task is null") ?: throw IllegalArgumentException("task is null")
private val _originalState = MutableStateFlow( private val _originalState = MutableStateFlow(
ViewState( TaskEditViewState(
task = task, task = task,
showBeastModeHint = !preferences.shownBeastModeHint, showBeastModeHint = !preferences.shownBeastModeHint,
showComments = preferences.getBoolean(R.string.p_show_task_edit_comments, false), showComments = preferences.getBoolean(R.string.p_show_task_edit_comments, false),
@ -157,6 +129,8 @@ class TaskEditViewModel @Inject constructor(
backButtonSavesTask = preferences.backButtonSavesTask(), backButtonSavesTask = preferences.backButtonSavesTask(),
isReadOnly = task.readOnly, isReadOnly = task.readOnly,
linkify = preferences.linkify, linkify = preferences.linkify,
alwaysDisplayFullDate = preferences.alwaysDisplayFullDate,
showEditScreenWithoutUnlock = preferences.showEditScreenWithoutUnlock,
calendar = if (task.isNew && permissionChecker.canAccessCalendars()) { calendar = if (task.isNew && permissionChecker.canAccessCalendars()) {
preferences.defaultCalendar preferences.defaultCalendar
} else { } else {
@ -202,10 +176,10 @@ class TaskEditViewModel @Inject constructor(
list = CaldavFilter(calendar = CaldavCalendar(), account = CaldavAccount()), list = CaldavFilter(calendar = CaldavCalendar(), account = CaldavAccount()),
) )
) )
val originalState: StateFlow<ViewState> = _originalState val originalState: StateFlow<TaskEditViewState> = _originalState
private val _viewState = MutableStateFlow(originalState.value) private val _viewState = MutableStateFlow(originalState.value)
val viewState: StateFlow<ViewState> = _viewState val viewState: StateFlow<TaskEditViewState> = _viewState
var eventUri = MutableStateFlow(task.calendarURI) var eventUri = MutableStateFlow(task.calendarURI)
val timerStarted = MutableStateFlow(task.timerStart) val timerStarted = MutableStateFlow(task.timerStart)
@ -298,6 +272,10 @@ class TaskEditViewModel @Inject constructor(
discard() discard()
return@withContext false return@withContext false
} }
if (!task.isNew && taskDeleter.isDeleted(task.id)) {
discard()
return@withContext false
}
clear() clear()
val viewState = _viewState.value val viewState = _viewState.value
val isNew = viewState.isNew val isNew = viewState.isNew

@ -104,4 +104,14 @@ WHERE recurring = 1
} }
deleteCaldavAccount(caldavAccount) deleteCaldavAccount(caldavAccount)
} }
@Query("""
SELECT CASE
WHEN deleted > 0 THEN 1
ELSE 0
END
FROM tasks
WHERE _id = :task
""")
abstract fun isDeleted(task: Long): Boolean
} }

@ -0,0 +1,21 @@
package org.tasks.kmp.org.tasks.extensions
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput
fun Modifier.gesturesDisabled(disabled: Boolean = true) =
if (disabled) {
pointerInput(Unit) {
awaitPointerEventScope {
while (true) {
awaitPointerEvent(pass = PointerEventPass.Initial)
.changes
.filter { it.position == it.previousPosition }
.forEach { it.consume() }
}
}
}
} else {
this
}

@ -0,0 +1,42 @@
package org.tasks.kmp.org.tasks.taskedit
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentSetOf
import org.tasks.data.Location
import org.tasks.data.entity.Alarm
import org.tasks.data.entity.TagData
import org.tasks.data.entity.Task
import org.tasks.data.entity.TaskAttachment
import org.tasks.filters.CaldavFilter
data class TaskEditViewState(
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 alwaysDisplayFullDate: Boolean,
val showEditScreenWithoutUnlock: 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 multilineTitle: Boolean,
) {
val isNew: Boolean
get() = task.isNew
val hasParent: Boolean
get() = task.parent > 0
val isCompleted: Boolean
get() = task.completionDate > 0
}
Loading…
Cancel
Save