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 d42adfe66..c3f99c06e 100644 --- a/app/src/main/java/com/todoroo/astrid/ui/ReminderControlSet.kt +++ b/app/src/main/java/com/todoroo/astrid/ui/ReminderControlSet.kt @@ -2,16 +2,18 @@ package com.todoroo.astrid.ui import android.Manifest import android.app.Activity +import android.app.Activity.RESULT_OK import android.content.DialogInterface import android.content.Intent import android.os.Bundle import android.view.View import android.view.ViewGroup +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.platform.ComposeView -import androidx.fragment.app.viewModels import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionStatus import com.google.accompanist.permissions.rememberPermissionState @@ -24,17 +26,11 @@ import org.tasks.compose.collectAsStateLifecycleAware import org.tasks.compose.edit.AlarmRow import org.tasks.data.Alarm import org.tasks.data.Alarm.Companion.TYPE_DATE_TIME -import org.tasks.data.Alarm.Companion.TYPE_REL_END -import org.tasks.data.Alarm.Companion.TYPE_REL_START -import org.tasks.data.Alarm.Companion.whenDue -import org.tasks.data.Alarm.Companion.whenOverdue -import org.tasks.data.Alarm.Companion.whenStarted import org.tasks.date.DateTimeUtils import org.tasks.dialogs.DialogBuilder import org.tasks.dialogs.MyTimePickerDialog import org.tasks.ui.TaskEditControlFragment import java.util.* -import java.util.concurrent.TimeUnit import javax.inject.Inject @AndroidEntryPoint @@ -44,7 +40,6 @@ class ReminderControlSet : TaskEditControlFragment() { @Inject lateinit var locale: Locale private val ringMode = mutableStateOf(0) - private val vm: ReminderControlSetViewModel by viewModels() override fun createView(savedInstanceState: Bundle?) { when { @@ -76,39 +71,6 @@ class ReminderControlSet : TaskEditControlFragment() { this.ringMode.value = ringMode } - private fun addAlarm(selected: String) { - val id = viewModel.task.id - when (selected) { - getString(R.string.when_started) -> - viewModel.addAlarm(whenStarted(id)) - getString(R.string.when_due) -> - viewModel.addAlarm(whenDue(id)) - getString(R.string.when_overdue) -> - viewModel.addAlarm(whenOverdue(id)) - getString(R.string.randomly) -> - vm.showRandomDialog(visible = true) - getString(R.string.pick_a_date_and_time) -> - addNewAlarm() - getString(R.string.repeat_option_custom) -> - vm.showCustomDialog(visible = true) - } - } - - private fun addAlarm() { - val options = options - if (options.size == 1) { - addNewAlarm() - } else { - dialogBuilder - .newDialog() - .setItems(options) { dialog: DialogInterface, which: Int -> - addAlarm(options[which]) - dialog.dismiss() - } - .show() - } - } - @OptIn(ExperimentalPermissionsApi::class) override fun bind(parent: ViewGroup?): View = (parent as ComposeView).apply { @@ -122,6 +84,16 @@ class ReminderControlSet : TaskEditControlFragment() { } else { null } + val pickDateAndTime = + rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode != RESULT_OK) return@rememberLauncherForActivityResult + val data = result.data ?: return@rememberLauncherForActivityResult + val timestamp = + data.getLongExtra(MyTimePickerDialog.EXTRA_TIMESTAMP, 0L) + val replace: Alarm? = data.getParcelableExtra(EXTRA_REPLACE) + replace?.let { viewModel.removeAlarm(it) } + viewModel.addAlarm(Alarm(0, timestamp, TYPE_DATE_TIME)) + } AlarmRow( locale = locale, alarms = viewModel.selectedAlarms.collectAsStateLifecycleAware().value, @@ -131,11 +103,18 @@ class ReminderControlSet : TaskEditControlFragment() { notificationPermissions?.launchPermissionRequest() }, ringMode = ringMode, - newAlarm = this@ReminderControlSet::addAlarm, addAlarm = viewModel::addAlarm, openRingType = this@ReminderControlSet::onClickRingType, - deleteAlarm = { - viewModel.selectedAlarms.value = viewModel.selectedAlarms.value.minus(it) + deleteAlarm = viewModel::removeAlarm, + pickDateAndTime = { replace -> + pickDateAndTime.launch( + Intent(activity, DateAndTimePickerActivity::class.java) + .putExtra( + DateAndTimePickerActivity.EXTRA_TIMESTAMP, + DateTimeUtils.newDateTime().noon().millis + ) + .putExtra(EXTRA_REPLACE, replace) + ) } ) } @@ -144,46 +123,8 @@ class ReminderControlSet : TaskEditControlFragment() { override fun controlId() = TAG - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == REQUEST_NEW_ALARM) { - if (resultCode == Activity.RESULT_OK) { - val timestamp = data!!.getLongExtra(MyTimePickerDialog.EXTRA_TIMESTAMP, 0L) - viewModel.addAlarm(Alarm(0, timestamp, TYPE_DATE_TIME)) - } - } else { - super.onActivityResult(requestCode, resultCode, data) - } - } - - private fun addNewAlarm() { - val intent = Intent(activity, DateAndTimePickerActivity::class.java) - .putExtra( - DateAndTimePickerActivity.EXTRA_TIMESTAMP, - DateTimeUtils.newDateTime().noon().millis - ) - startActivityForResult(intent, REQUEST_NEW_ALARM) - } - - private val options: List - get() { - val options: MutableList = ArrayList() - if (viewModel.selectedAlarms.value.find { it.type == TYPE_REL_START && it.time == 0L } == null) { - options.add(getString(R.string.when_started)) - } - if (viewModel.selectedAlarms.value.find { it.type == TYPE_REL_END && it.time == 0L } == null) { - options.add(getString(R.string.when_due)) - } - if (viewModel.selectedAlarms.value.find { it.type == TYPE_REL_END && it.time == TimeUnit.HOURS.toMillis(24) } == null) { - options.add(getString(R.string.when_overdue)) - } - options.add(getString(R.string.randomly)) - options.add(getString(R.string.pick_a_date_and_time)) - options.add(getString(R.string.repeat_option_custom)) - return options - } - companion object { const val TAG = R.string.TEA_ctrl_reminders_pref - private const val REQUEST_NEW_ALARM = 12152 + private const val EXTRA_REPLACE = "extra_replace" } } diff --git a/app/src/main/java/com/todoroo/astrid/ui/ReminderControlSetViewModel.kt b/app/src/main/java/com/todoroo/astrid/ui/ReminderControlSetViewModel.kt index 72ce9fc9e..e0bca1ce5 100644 --- a/app/src/main/java/com/todoroo/astrid/ui/ReminderControlSetViewModel.kt +++ b/app/src/main/java/com/todoroo/astrid/ui/ReminderControlSetViewModel.kt @@ -4,12 +4,16 @@ import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import org.tasks.data.Alarm class ReminderControlSetViewModel : ViewModel() { data class ViewState( + val showAddAlarm: Boolean = false, val showCustomDialog: Boolean = false, val showRandomDialog: Boolean = false, + val replace: Alarm? = null, ) private val _viewState = MutableStateFlow(ViewState()) @@ -17,15 +21,26 @@ class ReminderControlSetViewModel : ViewModel() { val viewState: StateFlow get() = _viewState.asStateFlow() + fun setReplace(alarm: Alarm?) { + _viewState.update { it.copy(replace = alarm) } + } + + fun showAddAlarm(visible: Boolean) { + _viewState.update { state -> + state.copy( + showAddAlarm = visible, + replace = state.replace?.takeIf { + visible || state.showCustomDialog || state.showRandomDialog + }, + ) + } + } + fun showCustomDialog(visible: Boolean) { - _viewState.value = _viewState.value.copy( - showCustomDialog = visible - ) + _viewState.update { it.copy(showCustomDialog = visible) } } fun showRandomDialog(visible: Boolean) { - _viewState.value = _viewState.value.copy( - showRandomDialog = visible - ) + _viewState.update { it.copy(showRandomDialog = visible) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/tasks/activities/DateAndTimePickerActivity.kt b/app/src/main/java/org/tasks/activities/DateAndTimePickerActivity.kt index d7e488be8..0504d707e 100644 --- a/app/src/main/java/org/tasks/activities/DateAndTimePickerActivity.kt +++ b/app/src/main/java/org/tasks/activities/DateAndTimePickerActivity.kt @@ -79,6 +79,7 @@ class DateAndTimePickerActivity : InjectingAppCompatActivity() { } addOnPositiveButtonClickListener { val data = Intent() + data.putExtras(intent) data.putExtra( MyTimePickerDialog.EXTRA_TIMESTAMP, DateTime( diff --git a/app/src/main/java/org/tasks/compose/AddReminderDialog.kt b/app/src/main/java/org/tasks/compose/AddReminderDialog.kt index 8e929cd9f..536a81267 100644 --- a/app/src/main/java/org/tasks/compose/AddReminderDialog.kt +++ b/app/src/main/java/org/tasks/compose/AddReminderDialog.kt @@ -32,6 +32,9 @@ import com.google.android.material.composethemeadapter.MdcTheme import kotlinx.coroutines.android.awaitFrame import org.tasks.R import org.tasks.data.Alarm +import org.tasks.data.Alarm.Companion.TYPE_REL_END +import org.tasks.data.Alarm.Companion.TYPE_REL_START +import org.tasks.data.Alarm.Companion.whenStarted import org.tasks.reminders.AlarmToString.Companion.getRepeatString import java.util.concurrent.TimeUnit @@ -104,11 +107,11 @@ object AddReminderDialog { time.value.takeIf { it >= 0 }?.let { i -> addAlarm( Alarm( - 0, - -1 * i * units.millis, - Alarm.TYPE_REL_END, - repeat.value, - interval.value * recurringUnits.millis + task = 0, + time = -1 * i * units.millis, + type = TYPE_REL_END, + repeat = repeat.value, + interval = interval.value * recurringUnits.millis ) ) closeDialog() @@ -460,6 +463,54 @@ fun BodyText(modifier: Modifier = Modifier, text: String) { ) } +@Composable +fun AlarmDropDown( + visible: Boolean, + existingAlarms: List, + addAlarm: (Alarm) -> Unit, + addRandom: () -> Unit, + addCustom: () -> Unit, + pickDateAndTime: () -> Unit, + dismiss: () -> Unit, +) { + CustomDialog(visible = visible, onDismiss = dismiss) { + Column(modifier = Modifier.padding(vertical = 4.dp)) { + if (existingAlarms.none { it.type == TYPE_REL_START && it.time == 0L }) { + DialogRow(text = R.string.when_started) { + addAlarm(whenStarted(0)) + dismiss() + } + } + if (existingAlarms.none { it.type == TYPE_REL_END && it.time == 0L }) { + DialogRow(text = R.string.when_due) { + addAlarm(Alarm.whenDue(0)) + dismiss() + } + } + if (existingAlarms.none { + it.type == TYPE_REL_END && it.time == TimeUnit.HOURS.toMillis(24) + }) { + DialogRow(text = R.string.when_overdue) { + addAlarm(Alarm.whenOverdue(0)) + dismiss() + } + } + DialogRow(text = R.string.randomly) { + addRandom() + dismiss() + } + DialogRow(text = R.string.pick_a_date_and_time) { + pickDateAndTime() + dismiss() + } + DialogRow(text = R.string.repeat_option_custom) { + addCustom() + dismiss() + } + } + } +} + @ExperimentalComposeUiApi @Preview(showBackground = true) @Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) @@ -546,4 +597,20 @@ fun AddRandomReminder() = time = remember { mutableStateOf(15) }, units = remember { mutableStateOf(1) } ) + } + +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +fun AddReminderDialog() = + MdcTheme { + AlarmDropDown( + visible = true, + existingAlarms = emptyList(), + addAlarm = {}, + addRandom = {}, + addCustom = {}, + pickDateAndTime = {}, + dismiss = {}, + ) } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/compose/CustomDialog.kt b/app/src/main/java/org/tasks/compose/CustomDialog.kt new file mode 100644 index 000000000..315e66e40 --- /dev/null +++ b/app/src/main/java/org/tasks/compose/CustomDialog.kt @@ -0,0 +1,29 @@ +package org.tasks.compose + +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Card +import androidx.compose.material.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog + +@Composable +fun CustomDialog( + visible: Boolean, + onDismiss: () -> Unit, + content: @Composable () -> Unit, +) { + if (!visible) { + return + } + Dialog(onDismissRequest = onDismiss) { + Card( + shape = MaterialTheme.shapes.medium, + modifier = Modifier.padding(16.dp), + elevation = 8.dp + ) { + content() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/tasks/compose/DialogRow.kt b/app/src/main/java/org/tasks/compose/DialogRow.kt new file mode 100644 index 000000000..780d233ef --- /dev/null +++ b/app/src/main/java/org/tasks/compose/DialogRow.kt @@ -0,0 +1,25 @@ +package org.tasks.compose + +import androidx.annotation.IntegerRes +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp + +@Composable +fun DialogRow(@IntegerRes text: Int, onClick: () -> Unit) { + Text( + text = stringResource(id = text), + modifier = Modifier + .clickable { onClick() } + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + color = MaterialTheme.colors.onSurface, + style = MaterialTheme.typography.body1, + ) +} diff --git a/app/src/main/java/org/tasks/compose/edit/AlarmRow.kt b/app/src/main/java/org/tasks/compose/edit/AlarmRow.kt index a8b4c8d2a..b34b800bd 100644 --- a/app/src/main/java/org/tasks/compose/edit/AlarmRow.kt +++ b/app/src/main/java/org/tasks/compose/edit/AlarmRow.kt @@ -37,10 +37,10 @@ fun AlarmRow( alarms: List, ringMode: Int, locale: Locale, - newAlarm: () -> Unit, addAlarm: (Alarm) -> Unit, deleteAlarm: (Alarm) -> Unit, openRingType: () -> Unit, + pickDateAndTime: (replace: Alarm?) -> Unit, ) { TaskEditRow( iconRes = R.drawable.ic_outline_notifications_24px, @@ -52,7 +52,11 @@ fun AlarmRow( alarms = alarms, ringMode = ringMode, locale = locale, - addAlarm = newAlarm, + replaceAlarm = { + vm.setReplace(it) + vm.showAddAlarm(visible = true) + }, + addAlarm = { vm.showAddAlarm(visible = true) }, deleteAlarm = deleteAlarm, openRingType = openRingType, ) @@ -62,8 +66,8 @@ fun AlarmRow( modifier = Modifier .padding(end = 16.dp) .clickable { - launchPermissionRequest() - } + launchPermissionRequest() + } ) { Spacer(modifier = Modifier.height(20.dp)) Text( @@ -80,15 +84,34 @@ fun AlarmRow( } } + AlarmDropDown( + visible = viewState.showAddAlarm, + existingAlarms = alarms, + addAlarm = { + viewState.replace?.let(deleteAlarm) + addAlarm(it) + }, + addRandom = { vm.showRandomDialog(visible = true) }, + addCustom = { vm.showCustomDialog(visible = true) }, + pickDateAndTime = { pickDateAndTime(viewState.replace) }, + dismiss = { vm.showAddAlarm(visible = false) }, + ) + AddReminderDialog.AddCustomReminderDialog( openDialog = viewState.showCustomDialog, - addAlarm = addAlarm, + addAlarm = { + viewState.replace?.let(deleteAlarm) + addAlarm(it) + }, closeDialog = { vm.showCustomDialog(visible = false) } ) AddReminderDialog.AddRandomReminderDialog( openDialog = viewState.showRandomDialog, - addAlarm = addAlarm, + addAlarm = { + viewState.replace?.let(deleteAlarm) + addAlarm(it) + }, closeDialog = { vm.showRandomDialog(visible = false) } ) }, @@ -100,6 +123,7 @@ fun Alarms( alarms: List, ringMode: Int, locale: Locale, + replaceAlarm: (Alarm) -> Unit, addAlarm: () -> Unit, deleteAlarm: (Alarm) -> Unit, openRingType: () -> Unit, @@ -107,9 +131,11 @@ fun Alarms( Column { Spacer(modifier = Modifier.height(8.dp)) alarms.forEach { alarm -> - AlarmRow(AlarmToString(LocalContext.current, locale).toString(alarm)) { - deleteAlarm(alarm) - } + AlarmRow( + text = AlarmToString(LocalContext.current, locale).toString(alarm), + onClick = { replaceAlarm(alarm) }, + remove = { deleteAlarm(alarm) } + ) } Row(modifier = Modifier.fillMaxWidth()) { DisabledText( @@ -119,7 +145,7 @@ fun Alarms( .clickable( interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(bounded = false), - onClick = { addAlarm() } + onClick = addAlarm, ) ) Spacer(modifier = Modifier.weight(1f)) @@ -150,10 +176,15 @@ fun Alarms( } @Composable -private fun AlarmRow(text: String, remove: () -> Unit = {}) { +private fun AlarmRow( + text: String, + onClick: () -> Unit, + remove: () -> Unit, +) { Row( modifier = Modifier .fillMaxWidth() + .clickable { onClick() } ) { Text( text = text, @@ -175,12 +206,12 @@ fun NoAlarms() { alarms = emptyList(), ringMode = 0, locale = Locale.getDefault(), - newAlarm = {}, addAlarm = {}, deleteAlarm = {}, openRingType = {}, permissionStatus = PermissionStatus.Granted, - launchPermissionRequest = {} + launchPermissionRequest = {}, + pickDateAndTime = {}, ) } } @@ -195,12 +226,12 @@ fun PermissionDenied() { alarms = emptyList(), ringMode = 0, locale = Locale.getDefault(), - newAlarm = {}, addAlarm = {}, deleteAlarm = {}, openRingType = {}, permissionStatus = PermissionStatus.Denied(true), - launchPermissionRequest = {} + launchPermissionRequest = {}, + pickDateAndTime = {}, ) } } \ 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 fefe335bb..99ce9b6f5 100644 --- a/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt +++ b/app/src/main/java/org/tasks/ui/TaskEditViewModel.kt @@ -28,6 +28,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext @@ -385,6 +386,10 @@ class TaskEditViewModel @Inject constructor( } } + fun removeAlarm(alarm: Alarm) { + selectedAlarms.update { it.minus(alarm) } + } + fun addAlarm(alarm: Alarm) { with (selectedAlarms) { if (value.none { it.same(alarm) }) {