diff --git a/app/src/main/java/com/todoroo/astrid/activity/MainActivity.kt b/app/src/main/java/com/todoroo/astrid/activity/MainActivity.kt index e906a7d75..5091e4e36 100644 --- a/app/src/main/java/com/todoroo/astrid/activity/MainActivity.kt +++ b/app/src/main/java/com/todoroo/astrid/activity/MainActivity.kt @@ -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(), diff --git a/app/src/main/java/com/todoroo/astrid/activity/TaskEditFragment.kt b/app/src/main/java/com/todoroo/astrid/activity/TaskEditFragment.kt index 1a9a6dc24..315a10f63 100755 --- a/app/src/main/java/com/todoroo/astrid/activity/TaskEditFragment.kt +++ b/app/src/main/java/com/todoroo/astrid/activity/TaskEditFragment.kt @@ -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() - StartDateControlSet.TAG -> AndroidFragment() - ReminderControlSet.TAG -> AndroidFragment() - LocationControlSet.TAG -> AndroidFragment() - FilesControlSet.TAG -> AndroidFragment() - TimerControlSet.TAG -> AndroidFragment() - TagsControlSet.TAG -> AndroidFragment() - RepeatControlSet.TAG -> AndroidFragment() - SubtaskControlSet.TAG -> AndroidFragment() - 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 - } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt b/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt index 318d7ab9d..6dd777aab 100644 --- a/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt +++ b/app/src/main/java/com/todoroo/astrid/activity/TaskListFragment.kt @@ -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? = 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() } diff --git a/app/src/main/java/com/todoroo/astrid/files/FilesControlSet.kt b/app/src/main/java/com/todoroo/astrid/files/FilesControlSet.kt index 59530464f..f58078593 100644 --- a/app/src/main/java/com/todoroo/astrid/files/FilesControlSet.kt +++ b/app/src/main/java/com/todoroo/astrid/files/FilesControlSet.kt @@ -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) { diff --git a/app/src/main/java/com/todoroo/astrid/repeats/RepeatControlSet.kt b/app/src/main/java/com/todoroo/astrid/repeats/RepeatControlSet.kt index ea2f9f06d..09be98832 100644 --- a/app/src/main/java/com/todoroo/astrid/repeats/RepeatControlSet.kt +++ b/app/src/main/java/com/todoroo/astrid/repeats/RepeatControlSet.kt @@ -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" diff --git a/app/src/main/java/com/todoroo/astrid/service/TaskDeleter.kt b/app/src/main/java/com/todoroo/astrid/service/TaskDeleter.kt index d19f7699d..0fb92cc24 100644 --- a/app/src/main/java/com/todoroo/astrid/service/TaskDeleter.kt +++ b/app/src/main/java/com/todoroo/astrid/service/TaskDeleter.kt @@ -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): List = withContext(NonCancellable) { @@ -107,4 +106,6 @@ class TaskDeleter @Inject constructor( } } } + + fun isDeleted(task: Long): Boolean = deletionDao.isDeleted(task) } diff --git a/app/src/main/java/com/todoroo/astrid/tags/TagsControlSet.kt b/app/src/main/java/com/todoroo/astrid/tags/TagsControlSet.kt index 0029be172..b6d31bf14 100644 --- a/app/src/main/java/com/todoroo/astrid/tags/TagsControlSet.kt +++ b/app/src/main/java/com/todoroo/astrid/tags/TagsControlSet.kt @@ -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) { diff --git a/app/src/main/java/com/todoroo/astrid/timers/TimerControlSet.kt b/app/src/main/java/com/todoroo/astrid/timers/TimerControlSet.kt index be46137ad..2ef0e9560 100644 --- a/app/src/main/java/com/todoroo/astrid/timers/TimerControlSet.kt +++ b/app/src/main/java/com/todoroo/astrid/timers/TimerControlSet.kt @@ -99,8 +99,6 @@ class TimerControlSet : TaskEditControlFragment() { } } - override fun controlId() = TAG - private fun timerActive() = viewModel.timerStarted.value > 0 private suspend fun stopTimer(): Task { diff --git a/app/src/main/java/com/todoroo/astrid/ui/ReminderControlSet.kt b/app/src/main/java/com/todoroo/astrid/ui/ReminderControlSet.kt index 4705cce42..17251ca92 100644 --- a/app/src/main/java/com/todoroo/astrid/ui/ReminderControlSet.kt +++ b/app/src/main/java/com/todoroo/astrid/ui/ReminderControlSet.kt @@ -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" diff --git a/app/src/main/java/com/todoroo/astrid/ui/StartDateControlSet.kt b/app/src/main/java/com/todoroo/astrid/ui/StartDateControlSet.kt index 2df498905..75303ac2a 100644 --- a/app/src/main/java/com/todoroo/astrid/ui/StartDateControlSet.kt +++ b/app/src/main/java/com/todoroo/astrid/ui/StartDateControlSet.kt @@ -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) { diff --git a/app/src/main/java/org/tasks/compose/edit/DueDateRow.kt b/app/src/main/java/org/tasks/compose/edit/DueDateRow.kt index 7a5f79921..e13686e79 100644 --- a/app/src/main/java/org/tasks/compose/edit/DueDateRow.kt +++ b/app/src/main/java/org/tasks/compose/edit/DueDateRow.kt @@ -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, diff --git a/app/src/main/java/org/tasks/compose/edit/TaskEditScreen.kt b/app/src/main/java/org/tasks/compose/edit/TaskEditScreen.kt index 97ef9e09c..d6ab47770 100644 --- a/app/src/main/java/org/tasks/compose/edit/TaskEditScreen.kt +++ b/app/src/main/java/org/tasks/compose/edit/TaskEditScreen.kt @@ -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, 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() + StartDateControlSet.TAG -> AndroidFragment() + ReminderControlSet.TAG -> AndroidFragment() + LocationControlSet.TAG -> AndroidFragment() + FilesControlSet.TAG -> AndroidFragment() + TimerControlSet.TAG -> AndroidFragment() + TagsControlSet.TAG -> AndroidFragment() + RepeatControlSet.TAG -> AndroidFragment() + SubtaskControlSet.TAG -> AndroidFragment() + 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, ) } diff --git a/app/src/main/java/org/tasks/injection/ActivityRetainedModule.kt b/app/src/main/java/org/tasks/injection/ActivityRetainedModule.kt index 96f6109e3..e7ba93263 100644 --- a/app/src/main/java/org/tasks/injection/ActivityRetainedModule.kt +++ b/app/src/main/java/org/tasks/injection/ActivityRetainedModule.kt @@ -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 makeFlow() = MutableSharedFlow( extraBufferCapacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST diff --git a/app/src/main/java/org/tasks/ui/CalendarControlSet.kt b/app/src/main/java/org/tasks/ui/CalendarControlSet.kt index c8c9c7f1d..fcdd6122e 100644 --- a/app/src/main/java/org/tasks/ui/CalendarControlSet.kt +++ b/app/src/main/java/org/tasks/ui/CalendarControlSet.kt @@ -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) diff --git a/app/src/main/java/org/tasks/ui/LocationControlSet.kt b/app/src/main/java/org/tasks/ui/LocationControlSet.kt index a1684106d..7f5d3d205 100644 --- a/app/src/main/java/org/tasks/ui/LocationControlSet.kt +++ b/app/src/main/java/org/tasks/ui/LocationControlSet.kt @@ -105,8 +105,6 @@ class LocationControlSet : TaskEditControlFragment() { } } - override fun controlId() = TAG - private fun openWebsite() { viewModel.viewState.value.location?.let { context?.openUri(it.url) } } diff --git a/app/src/main/java/org/tasks/ui/SubtaskControlSet.kt b/app/src/main/java/org/tasks/ui/SubtaskControlSet.kt index bda67fc9e..b295933fa 100644 --- a/app/src/main/java/org/tasks/ui/SubtaskControlSet.kt +++ b/app/src/main/java/org/tasks/ui/SubtaskControlSet.kt @@ -89,8 +89,6 @@ class SubtaskControlSet : TaskEditControlFragment() { } } - override fun controlId() = TAG - private fun openSubtask(task: Task) = lifecycleScope.launch { mainViewModel.setTask(task) } diff --git a/app/src/main/java/org/tasks/ui/TaskEditControlFragment.kt b/app/src/main/java/org/tasks/ui/TaskEditControlFragment.kt index 96277a283..0705eb6ff 100644 --- a/app/src/main/java/org/tasks/ui/TaskEditControlFragment.kt +++ b/app/src/main/java/org/tasks/ui/TaskEditControlFragment.kt @@ -26,6 +26,4 @@ abstract class TaskEditControlFragment : Fragment() { abstract fun bind(parent: ViewGroup?): View protected open fun createView(savedInstanceState: Bundle?) {} - - abstract fun controlId(): Int } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/ui/TaskEditEvent.kt b/app/src/main/java/org/tasks/ui/TaskEditEvent.kt deleted file mode 100644 index 8d404096c..000000000 --- a/app/src/main/java/org/tasks/ui/TaskEditEvent.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.tasks.ui - -import kotlinx.coroutines.flow.MutableSharedFlow - -typealias TaskEditEventBus = MutableSharedFlow - -sealed interface TaskEditEvent { - data class Discard(val id: Long) : TaskEditEvent -} \ No newline at end of file diff --git a/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt b/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt index bc8b7a1e2..c0ee652a1 100644 --- a/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt +++ b/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt @@ -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, - 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, - val calendar: String?, - val attachments: ImmutableSet = persistentSetOf(), - val alarms: ImmutableSet, - val newSubtasks: ImmutableList = 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 = _originalState + val originalState: StateFlow = _originalState private val _viewState = MutableStateFlow(originalState.value) - val viewState: StateFlow = _viewState + val viewState: StateFlow = _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 diff --git a/data/src/commonMain/kotlin/org/tasks/data/dao/DeletionDao.kt b/data/src/commonMain/kotlin/org/tasks/data/dao/DeletionDao.kt index 103bede72..9e78ac63e 100644 --- a/data/src/commonMain/kotlin/org/tasks/data/dao/DeletionDao.kt +++ b/data/src/commonMain/kotlin/org/tasks/data/dao/DeletionDao.kt @@ -104,4 +104,14 @@ WHERE recurring = 1 } deleteCaldavAccount(caldavAccount) } -} \ No newline at end of file + + @Query(""" + SELECT CASE + WHEN deleted > 0 THEN 1 + ELSE 0 + END + FROM tasks + WHERE _id = :task + """) + abstract fun isDeleted(task: Long): Boolean +} diff --git a/kmp/src/commonMain/kotlin/org/tasks/extensions/Modifier.kt b/kmp/src/commonMain/kotlin/org/tasks/extensions/Modifier.kt new file mode 100644 index 000000000..237c5d6fe --- /dev/null +++ b/kmp/src/commonMain/kotlin/org/tasks/extensions/Modifier.kt @@ -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 + } diff --git a/kmp/src/commonMain/kotlin/org/tasks/taskedit/TaskEditViewState.kt b/kmp/src/commonMain/kotlin/org/tasks/taskedit/TaskEditViewState.kt new file mode 100644 index 000000000..ef0e56d6d --- /dev/null +++ b/kmp/src/commonMain/kotlin/org/tasks/taskedit/TaskEditViewState.kt @@ -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, + 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, + val calendar: String?, + val attachments: ImmutableSet = persistentSetOf(), + val alarms: ImmutableSet, + val newSubtasks: ImmutableList = persistentListOf(), + val multilineTitle: Boolean, +) { + val isNew: Boolean + get() = task.isNew + + val hasParent: Boolean + get() = task.parent > 0 + + val isCompleted: Boolean + get() = task.completionDate > 0 +}