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

@ -4,75 +4,34 @@ import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.core.os.BundleCompat
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels
import androidx.fragment.compose.AndroidFragment
import androidx.fragment.compose.content
import androidx.lifecycle.compose.collectAsStateWithLifecycle
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 kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.tasks.R
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.TitleRow
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.DialogBuilder
import org.tasks.dialogs.Linkify
import org.tasks.extensions.Context.is24HourFormat
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.notifications.NotificationManager
import org.tasks.play.PlayServices
import org.tasks.preferences.Preferences
import org.tasks.themes.TasksTheme
import org.tasks.themes.Theme
import org.tasks.ui.CalendarControlSet
import org.tasks.ui.ChipProvider
import org.tasks.ui.LocationControlSet
import org.tasks.ui.SubtaskControlSet
import org.tasks.ui.TaskEditEvent
import org.tasks.ui.TaskEditEventBus
import org.tasks.ui.TaskEditViewModel
import org.tasks.ui.TaskEditViewModel.Companion.TAG_CREATION
import org.tasks.ui.TaskEditViewModel.Companion.TAG_DESCRIPTION
import org.tasks.ui.TaskEditViewModel.Companion.TAG_DUE_DATE
import org.tasks.ui.TaskEditViewModel.Companion.TAG_LIST
import org.tasks.ui.TaskEditViewModel.Companion.TAG_PRIORITY
import org.tasks.ui.TaskEditViewModel.Companion.TAG_TITLE
import java.util.Locale
import javax.inject.Inject
@ -83,8 +42,6 @@ class TaskEditFragment : Fragment() {
@Inject lateinit var dialogBuilder: DialogBuilder
@Inject lateinit var preferences: Preferences
@Inject lateinit var linkify: Linkify
@Inject lateinit var markdownProvider: MarkdownProvider
@Inject lateinit var taskEditEventBus: TaskEditEventBus
@Inject lateinit var locale: Locale
@Inject lateinit var chipProvider: ChipProvider
@Inject lateinit var playServices: PlayServices
@ -92,27 +49,12 @@ class TaskEditFragment : Fragment() {
private val editViewModel: TaskEditViewModel by viewModels()
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(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = content {
LaunchedEffect(Unit) {
if (atLeastOreoMR1()) {
activity?.setShowWhenLocked(preferences.showEditScreenWithoutUnlock)
}
}
TasksTheme(theme = theme.themeBase.index,) {
val viewState = editViewModel.viewState.collectAsStateWithLifecycle().value
LaunchedEffect(viewState.isNew) {
@ -120,27 +62,39 @@ class TaskEditFragment : Fragment() {
notificationManager.cancel(viewState.task.id)
}
}
val context = LocalContext.current
TaskEditScreen(
editViewModel = editViewModel,
viewState = viewState,
comments = userActivityDao
.watchComments(viewState.task.uuid)
.collectAsStateWithLifecycle(emptyList())
.value,
save = { lifecycleScope.launch { save() } },
discard = { discardButtonClick() },
onBackPressed = {
if (viewState.backButtonSavesTask) {
lifecycleScope.launch {
save()
}
discard = {
activity?.hideKeyboard()
if (editViewModel.hasChanges()) {
dialogBuilder
.newDialog(R.string.discard_confirmation)
.setPositiveButton(R.string.keep_editing, null)
.setNegativeButton(R.string.discard) { _, _ -> discard() }
.show()
} else {
discardButtonClick()
discard()
}
},
delete = { deleteButtonClick() },
openBeastModeSettings = {
editViewModel.hideBeastModeHint(click = true)
beastMode.launch(Intent(context, BeastModePreferences::class.java))
delete = {
activity?.hideKeyboard()
dialogBuilder
.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) },
deleteComment = {
@ -148,85 +102,25 @@ class TaskEditFragment : Fragment() {
userActivityDao.delete(it)
}
},
) { tag ->
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) },
markdownProvider = remember { MarkdownProvider(context, preferences) },
linkify = if (viewState.linkify) linkify else null,
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,
)
TAG_DESCRIPTION ->
DescriptionRow(
text = viewState.task.notes,
onChanged = { text -> editViewModel.setDescription(text.toString().trim { it <= ' ' }) },
linkify = if (viewState.linkify) linkify else null,
markdownProvider = markdownProvider,
)
TAG_LIST ->
ListRow(
list = viewState.list,
colorProvider = { chipProvider.getColor(it) },
onClick = {
listPickerLauncher.launch(
context = context,
selectedFilter = viewState.list,
listsOnly = true
)
}
)
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()
}
.show(parentFragmentManager, FRAG_TAG_DATE_PICKER)
},
colorProvider = { chipProvider.getColor(it) },
locale = remember { locale },
)
}
}
@ -238,38 +132,11 @@ class TaskEditFragment : Fragment() {
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 {
editViewModel.discard()
mainViewModel.setTask(null)
}
private fun delete() = lifecycleScope.launch {
editViewModel.delete()
mainViewModel.setTask(null)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
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 {
const val EXTRA_TASK = "extra_task"
@ -357,28 +160,5 @@ class TaskEditFragment : Fragment() {
private const val FRAG_TAG_DATE_PICKER = "frag_tag_date_picker"
const val REQUEST_CODE_PICK_CALENDAR = 70
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
import android.Manifest
import android.app.Activity
import android.app.Activity.RESULT_OK
import android.content.BroadcastReceiver
import android.content.Context
@ -143,8 +142,6 @@ import org.tasks.themes.Theme
import org.tasks.themes.ThemeColor
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import org.tasks.ui.Banner
import org.tasks.ui.TaskEditEvent
import org.tasks.ui.TaskEditEventBus
import org.tasks.ui.TaskListEvent
import org.tasks.ui.TaskListEventBus
import org.tasks.ui.TaskListViewModel
@ -181,7 +178,6 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
@Inject lateinit var firebase: Firebase
@Inject lateinit var repeatTaskHelper: RepeatTaskHelper
@Inject lateinit var taskListEventBus: TaskListEventBus
@Inject lateinit var taskEditEventBus: TaskEditEventBus
@Inject lateinit var database: Database
@Inject lateinit var markdown: MarkdownProvider
@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?) {
when (requestCode) {
VOICE_RECOGNITION_REQUEST_CODE -> if (resultCode == Activity.RESULT_OK) {
VOICE_RECOGNITION_REQUEST_CODE -> if (resultCode == RESULT_OK) {
lifecycleScope.launch {
val match: List<String>? = data!!.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)
if (!match.isNullOrEmpty() && match[0].isNotEmpty()) {
@ -961,7 +950,14 @@ class TaskListFragment : Fragment(), OnRefreshListener, Toolbar.OnMenuItemClickL
val result = withContext(NonCancellable) {
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()
}

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

@ -98,8 +98,6 @@ class RepeatControlSet : TaskEditControlFragment() {
}
}
override fun controlId() = TAG
companion object {
val TAG = R.string.TEA_ctrl_repeat_pref
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 locationDao: LocationDao,
) {
suspend fun markDeleted(item: Task) = markDeleted(listOf(item.id))
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?) {
if (requestCode == REQUEST_TAG_PICKER_ACTIVITY) {
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 suspend fun stopTimer(): Task {

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

@ -89,8 +89,6 @@ class StartDateControlSet : TaskEditControlFragment() {
}
}
override fun controlId() = TAG
@Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.runBlocking
import org.tasks.R
import org.tasks.compose.DisabledText
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
@Composable
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?,
overdue: Boolean,
onClick: () -> Unit,

@ -1,7 +1,10 @@
package org.tasks.compose.edit
import android.content.Intent
import android.content.res.Configuration.UI_MODE_NIGHT_YES
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@ -22,36 +25,90 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.rememberCoroutineScope
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.core.content.IntentCompat.getParcelableExtra
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.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.dialogs.Linkify
import org.tasks.extensions.Context.findActivity
import org.tasks.extensions.Context.is24HourFormat
import org.tasks.files.FileHelper
import org.tasks.filters.CaldavFilter
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.ui.CalendarControlSet
import org.tasks.ui.LocationControlSet
import org.tasks.ui.SubtaskControlSet
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 timber.log.Timber
import java.util.Locale
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TaskEditScreen(
viewState: TaskEditViewModel.ViewState,
editViewModel: TaskEditViewModel,
viewState: TaskEditViewState,
comments: List<UserActivity>,
save: () -> Unit,
discard: () -> Unit,
onBackPressed: () -> Unit,
delete: () -> Unit,
openBeastModeSettings: () -> Unit,
dismissBeastMode: () -> 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 {
Timber.d("onBackPressed")
onBackPressed()
@ -118,12 +175,98 @@ fun TaskEditScreen(
.fillMaxSize()
.verticalScroll(rememberScrollState()),
) {
val scope = rememberCoroutineScope()
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()
}
if (viewState.showComments) {
val context = LocalContext.current
CommentsRow(
comments = comments,
copyCommentToClipboard = { copyToClipboard(context, R.string.comment, it) },
@ -131,9 +274,17 @@ fun TaskEditScreen(
openImage = { FileHelper.startActionView(context, it) },
)
}
val beastMode = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) {
context.findActivity()?.recreate()
}
BeastModeBanner(
visible = viewState.showBeastModeHint,
showSettings = openBeastModeSettings,
showSettings = {
editViewModel.hideBeastModeHint(click = true)
beastMode.launch(Intent(context, BeastModePreferences::class.java))
},
dismiss = dismissBeastMode,
)
}

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

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

@ -105,8 +105,6 @@ class LocationControlSet : TaskEditControlFragment() {
}
}
override fun controlId() = TAG
private fun openWebsite() {
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 {
mainViewModel.setTask(task)
}

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

@ -104,4 +104,14 @@ WHERE recurring = 1
}
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