From bfe0af5500af534d5299eaca2f25d9593af454d2 Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Tue, 8 Feb 2022 22:54:08 -0600 Subject: [PATCH] New relative reminder picker Only supports 'before due' for now --- .../todoroo/astrid/ui/ReminderControlSet.kt | 85 +++++++- .../org/tasks/compose/AddReminderDialog.kt | 186 ++++++++++++++++++ .../java/org/tasks/compose/PrincipalList.kt | 16 +- .../java/org/tasks/reminders/AlarmToString.kt | 19 +- .../main/res/layout/control_set_reminders.xml | 5 + app/src/main/res/values/strings.xml | 17 ++ 6 files changed, 313 insertions(+), 15 deletions(-) create mode 100644 app/src/main/java/org/tasks/compose/AddReminderDialog.kt 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 8d372e732..dbcea9eca 100644 --- a/app/src/main/java/com/todoroo/astrid/ui/ReminderControlSet.kt +++ b/app/src/main/java/com/todoroo/astrid/ui/ReminderControlSet.kt @@ -15,9 +15,18 @@ import android.view.ViewGroup import android.widget.LinearLayout import android.widget.TextView import androidx.annotation.StringRes +import androidx.compose.material.AlertDialog +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.ExperimentalComposeUiApi +import com.google.android.material.composethemeadapter.MdcTheme +import com.todoroo.andlib.utility.AndroidUtilities import dagger.hilt.android.AndroidEntryPoint import org.tasks.R import org.tasks.activities.DateAndTimePickerActivity +import org.tasks.compose.AddReminderDialog +import org.tasks.compose.Constants import org.tasks.data.Alarm import org.tasks.data.Alarm.Companion.TYPE_DATE_TIME import org.tasks.data.Alarm.Companion.TYPE_RANDOM @@ -35,6 +44,7 @@ import org.tasks.ui.TaskEditControlFragment import java.util.concurrent.TimeUnit import javax.inject.Inject + /** * Control set dealing with reminder settings * @@ -50,8 +60,10 @@ class ReminderControlSet : TaskEditControlFragment() { private lateinit var mode: TextView private var randomControlSet: RandomReminderControlSet? = null + private val showDialog = mutableStateOf(false) override fun createView(savedInstanceState: Bundle?) { + showDialog.value = savedInstanceState?.getBoolean(DIALOG_VISIBLE) ?: false mode.paintFlags = mode.paintFlags or Paint.UNDERLINE_TEXT_FLAG when { viewModel.ringNonstop!! -> setRingMode(2) @@ -61,6 +73,12 @@ class ReminderControlSet : TaskEditControlFragment() { viewModel.selectedAlarms?.forEach(this::addAlarmRow) } + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + + outState.putBoolean(DIALOG_VISIBLE, showDialog.value) + } + private fun onClickRingType() { val modes = resources.getStringArray(R.array.reminder_ring_modes) val ringMode = when { @@ -105,6 +123,8 @@ class ReminderControlSet : TaskEditControlFragment() { addAlarmRow(Alarm(id, TimeUnit.DAYS.toMillis(14), TYPE_RANDOM)) getString(R.string.pick_a_date_and_time) -> addNewAlarm() + getString(R.string.repeat_option_custom) -> + addCustomAlarm() } } @@ -123,6 +143,7 @@ class ReminderControlSet : TaskEditControlFragment() { } } + @OptIn(ExperimentalComposeUiApi::class) override fun bind(parent: ViewGroup?) = ControlSetRemindersBinding.inflate(layoutInflater, parent, true).let { alertContainer = it.alertContainer @@ -130,6 +151,61 @@ class ReminderControlSet : TaskEditControlFragment() { setOnClickListener { onClickRingType() } } it.alarmsAdd.setOnClickListener { addAlarm() } + it.dialogView.setContent { + MdcTheme { + val openDialog = remember { showDialog } + val selectedInterval = rememberSaveable { mutableStateOf(15L as Long?) } + val selectedMultiplier = rememberSaveable { mutableStateOf(0) } + if (openDialog.value) { + AlertDialog( + onDismissRequest = { + openDialog.value = false + AndroidUtilities.hideKeyboard(activity) + }, + text = { + AddReminderDialog.AddReminderDialog( + openDialog, + selectedInterval, + selectedMultiplier, + ) + }, + confirmButton = { + Constants.TextButton(text = R.string.ok, onClick = { + val multiplier = -1 * when (selectedMultiplier.value) { + 1 -> TimeUnit.HOURS.toMillis(1) + 2 -> TimeUnit.DAYS.toMillis(1) + 3 -> TimeUnit.DAYS.toMillis(7) + else -> TimeUnit.MINUTES.toMillis(1) + } + + selectedInterval.value?.let { i -> + addAlarmRow( + Alarm( + viewModel.task?.id ?: 0L, + i * multiplier, + TYPE_REL_END + ) + ) + openDialog.value = false + AndroidUtilities.hideKeyboard(activity) + } + }) + }, + dismissButton = { + Constants.TextButton( + text = R.string.cancel, + onClick = { + openDialog.value = false + AndroidUtilities.hideKeyboard(activity) + }) + }, + ) + } else { + selectedInterval.value = 15 + selectedMultiplier.value = 0 + } + } + } it.root } @@ -178,6 +254,10 @@ class ReminderControlSet : TaskEditControlFragment() { startActivityForResult(intent, REQUEST_NEW_ALARM) } + private fun addCustomAlarm() { + showDialog.value = true + } + private fun addAlarmRow(alarm: Alarm, onRemove: View.OnClickListener): View { val alertItem = requireActivity().layoutInflater.inflate(R.layout.alarm_edit_row, null) alertContainer.addView(alertItem) @@ -213,11 +293,14 @@ class ReminderControlSet : TaskEditControlFragment() { 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 DIALOG_VISIBLE = "dialog_visible" } -} \ No newline at end of file +} + diff --git a/app/src/main/java/org/tasks/compose/AddReminderDialog.kt b/app/src/main/java/org/tasks/compose/AddReminderDialog.kt new file mode 100644 index 000000000..ba9488806 --- /dev/null +++ b/app/src/main/java/org/tasks/compose/AddReminderDialog.kt @@ -0,0 +1,186 @@ +package org.tasks.compose + +import android.content.res.Configuration +import androidx.annotation.StringRes +import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.RadioButton +import androidx.compose.material.Text +import androidx.compose.material.TextFieldDefaults +import androidx.compose.material.darkColors +import androidx.compose.material.lightColors +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment.Companion.CenterVertically +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.android.awaitFrame +import org.tasks.R + +@ExperimentalComposeUiApi +object AddReminderDialog { + @Composable + fun AddReminderDialog( + visible: MutableState = mutableStateOf(true), + interval: MutableState, + selected: MutableState, + ) { + val scrollState = rememberScrollState() + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(scrollState) + ) { + CenteredH6(resId = R.string.custom_notification) + val focusRequester = remember { FocusRequester() } + OutlinedLongInput(interval, focusRequester) + Spacer(modifier = Modifier.height(16.dp)) + val options = listOf( + R.plurals.reminder_minutes, + R.plurals.reminder_hours, + R.plurals.reminder_days, + R.plurals.reminder_week, + ) + options.forEachIndexed { index, option -> + RadioRow(index, option, interval, selected) + } + ShowKeyboard(visible, focusRequester) + } + } +} + +@ExperimentalComposeUiApi +@Composable +fun ShowKeyboard(visible: MutableState, focusRequester: FocusRequester) { + val keyboardController = LocalSoftwareKeyboardController.current + LaunchedEffect(visible) { + focusRequester.freeFocus() + awaitFrame() + focusRequester.requestFocus() + keyboardController?.show() + } +} + +@Composable +fun OutlinedLongInput( + interval: MutableState, + focusRequester: FocusRequester +) { + val value = rememberSaveable(stateSaver = TextFieldValue.Saver) { + val text = interval.value.toString() + mutableStateOf(TextFieldValue(text = text, selection = TextRange(0, text.length))) + } + OutlinedTextField( + value = value.value, + onValueChange = { + value.value = it.copy(text = it.text.filter { t -> t.isDigit() }) + interval.value = value.value.text.toLongOrNull() + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .focusRequester(focusRequester), + colors = TextFieldDefaults.outlinedTextFieldColors( + textColor = MaterialTheme.colors.onSurface, + focusedBorderColor = MaterialTheme.colors.onSurface + ), + isError = value.value.text.toLongOrNull() == null, + ) +} + +@Composable +fun CenteredH6(@StringRes resId: Int) { + Text( + text = stringResource(id = resId), + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + color = MaterialTheme.colors.onSurface, + style = MaterialTheme.typography.h6 + ) +} + +@Composable +fun RadioRow(index: Int, option: Int, interval: MutableState, selected: MutableState) { + val number = interval.value?.toInt() ?: 1 + val optionString = LocalContext.current.resources.getQuantityString(option, number) + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + selected.value = index + } + ) { + RadioButton( + selected = index == selected.value, + onClick = { + selected.value = index + }, + modifier = Modifier + .padding(16.dp, 8.dp) + .align(CenterVertically) + ) + Text( + text = if (index == selected.value) { + stringResource(id = R.string.alarm_before_due, optionString) + } else { + optionString + }, + modifier = Modifier.align(CenterVertically), + color = MaterialTheme.colors.onSurface, + style = MaterialTheme.typography.body1, + ) + } +} + +@ExperimentalComposeUiApi +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +fun AddReminderOne() = + MaterialTheme(if (isSystemInDarkTheme()) darkColors() else lightColors()) { + AddReminderDialog.AddReminderDialog( + interval = mutableStateOf(1L), + selected = mutableStateOf(0) + ) + } + +@ExperimentalComposeUiApi +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +fun AddReminderMultiple() = + MaterialTheme(if (isSystemInDarkTheme()) darkColors() else lightColors()) { + AddReminderDialog.AddReminderDialog( + interval = mutableStateOf(15L), + selected = mutableStateOf(1) + ) + } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/compose/PrincipalList.kt b/app/src/main/java/org/tasks/compose/PrincipalList.kt index 8ff42a695..f1e50172d 100644 --- a/app/src/main/java/org/tasks/compose/PrincipalList.kt +++ b/app/src/main/java/org/tasks/compose/PrincipalList.kt @@ -1,5 +1,6 @@ package org.tasks.compose +import android.content.res.Configuration.UI_MODE_NIGHT_YES import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -15,7 +16,6 @@ import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme.colors import androidx.compose.material.Text -import androidx.compose.material.darkColors import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -49,21 +49,17 @@ private val principals = listOf( ) ) -@Preview(showBackground = true, backgroundColor = 0xFFFFFF) +@Preview(showBackground = true) +@Preview(showBackground = true, backgroundColor = 0x202124, uiMode = UI_MODE_NIGHT_YES) @Composable private fun Owner() = MaterialTheme { ListSettingsComposables.PrincipalList(principals) {} } -@Preview(showBackground = true, backgroundColor = 0x202124) +@Preview(showBackground = true) +@Preview(showBackground = true, backgroundColor = 0x202124, uiMode = UI_MODE_NIGHT_YES) @Composable -private fun OwnerDark() = MaterialTheme(darkColors()) { - ListSettingsComposables.PrincipalList(principals) {} -} - -@Preview(showBackground = true, backgroundColor = 0xFFFFFF) -@Composable -private fun NotOwner() = MaterialTheme { +private fun NotOwner() = MaterialTheme() { ListSettingsComposables.PrincipalList(principals, null) } diff --git a/app/src/main/java/org/tasks/reminders/AlarmToString.kt b/app/src/main/java/org/tasks/reminders/AlarmToString.kt index a7ec1ba0f..59cf89fe7 100644 --- a/app/src/main/java/org/tasks/reminders/AlarmToString.kt +++ b/app/src/main/java/org/tasks/reminders/AlarmToString.kt @@ -62,13 +62,24 @@ class AlarmToString @Inject constructor( private fun getDurationString(duration: Long): String { val seconds = duration.absoluteValue - val day = TimeUnit.MILLISECONDS.toDays(seconds) - val hours = TimeUnit.MILLISECONDS.toHours(seconds) - day * 24 + val days = TimeUnit.MILLISECONDS.toDays(seconds) + val weeks = days / 7 + val hours = TimeUnit.MILLISECONDS.toHours(seconds) - days * 24 val minute = TimeUnit.MILLISECONDS.toMinutes(seconds) - TimeUnit.MILLISECONDS.toHours(seconds) * 60 val result = ArrayList() - if (day > 0) { - result.add(resources.getQuantityString(R.plurals.repeat_n_days, day.toInt(), day.toInt())) + if (weeks > 0) { + result.add(resources.getQuantityString(R.plurals.repeat_n_weeks, weeks.toInt(), weeks.toInt())) + } + val leftoverDays = days - weeks * 7 + if (leftoverDays > 0) { + result.add( + resources.getQuantityString( + R.plurals.repeat_n_days, + leftoverDays.toInt(), + leftoverDays.toInt() + ) + ) } if (hours > 0) { result.add(resources.getQuantityString(R.plurals.repeat_n_hours, hours.toInt(), hours.toInt())) diff --git a/app/src/main/res/layout/control_set_reminders.xml b/app/src/main/res/layout/control_set_reminders.xml index 0810639f6..713a832b5 100644 --- a/app/src/main/res/layout/control_set_reminders.xml +++ b/app/src/main/res/layout/control_set_reminders.xml @@ -31,4 +31,9 @@ android:layout_height="wrap_content" android:hint="@string/add_reminder" /> + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index af3d809e5..7f7da3e08 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -209,6 +209,10 @@ File %1$s contained %2$s.\n\n minute minutes + + Minute + Minutes + %d minute %d minutes @@ -217,6 +221,10 @@ File %1$s contained %2$s.\n\n hour hours + + Hour + Hours + %d hour %d hours @@ -225,6 +233,10 @@ File %1$s contained %2$s.\n\n day days + + Day + Days + %d day %d days @@ -233,6 +245,10 @@ File %1$s contained %2$s.\n\n week weeks + + Week + Weeks + %d week %d weeks @@ -722,4 +738,5 @@ File %1$s contained %2$s.\n\n %s before due %s after due Snoozed until %s + Custom notification