Refactor custom and random reminder dialogs

pull/3995/head
Alex Baker 3 weeks ago
parent 9fbe27345d
commit 40961dad87

@ -25,10 +25,11 @@ import androidx.compose.material3.RadioButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
@ -62,141 +63,99 @@ import java.util.concurrent.TimeUnit
@ExperimentalComposeUiApi
object AddReminderDialog {
// Helper functions for converting between Alarm properties and UI state
private fun unitIndexToMillis(unitIndex: Int): Long = when (unitIndex) {
1 -> TimeUnit.HOURS.toMillis(1)
2 -> TimeUnit.DAYS.toMillis(1)
3 -> TimeUnit.DAYS.toMillis(7)
else -> TimeUnit.MINUTES.toMillis(1)
}
private fun timeToAmountAndUnit(time: Long): Pair<Int, Int> {
val absTime = kotlin.math.abs(time)
return when {
absTime == 0L -> 0 to 0 // Default to minutes when time is 0
absTime % TimeUnit.DAYS.toMillis(7) == 0L ->
(absTime / TimeUnit.DAYS.toMillis(7)).toInt() to 3
absTime % TimeUnit.DAYS.toMillis(1) == 0L ->
(absTime / TimeUnit.DAYS.toMillis(1)).toInt() to 2
absTime % TimeUnit.HOURS.toMillis(1) == 0L ->
(absTime / TimeUnit.HOURS.toMillis(1)).toInt() to 1
else ->
(absTime / TimeUnit.MINUTES.toMillis(1)).toInt() to 0
}
}
@Composable
fun AddRandomReminderDialog(
viewState: ViewState,
addAlarm: (Alarm) -> Unit,
alarm: Alarm?,
updateAlarm: (Alarm) -> Unit,
closeDialog: () -> Unit,
) {
val time = rememberSaveable { mutableStateOf(15) }
val units = rememberSaveable { mutableStateOf(0) }
if (viewState.showRandomDialog) {
AlertDialog(
onDismissRequest = closeDialog,
text = { AddRandomReminder(time, units) },
confirmButton = {
Constants.TextButton(text = R.string.ok, onClick = {
time.value.takeIf { it > 0 }?.let { i ->
addAlarm(Alarm(time = i * units.millis, type = TYPE_RANDOM))
closeDialog()
}
})
},
dismissButton = {
Constants.TextButton(
text = R.string.cancel,
onClick = closeDialog
)
},
)
} else {
time.value = 15
units.value = 0
// Create working copy from alarm or use defaults
var workingCopy by rememberSaveable {
mutableStateOf(alarm ?: Alarm(time = 15 * TimeUnit.MINUTES.toMillis(1), type = TYPE_RANDOM))
}
AlertDialog(
onDismissRequest = closeDialog,
text = {
AddRandomReminder(
alarm = workingCopy,
updateAlarm = { workingCopy = it }
)
},
confirmButton = {
Constants.TextButton(text = R.string.ok, onClick = {
val (amount, _) = timeToAmountAndUnit(workingCopy.time)
if (amount > 0) {
updateAlarm(workingCopy)
closeDialog()
}
})
},
dismissButton = {
Constants.TextButton(
text = R.string.cancel,
onClick = closeDialog
)
},
)
}
@Composable
fun AddCustomReminderDialog(
viewState: ViewState,
addAlarm: (Alarm) -> Unit,
alarm: Alarm?,
updateAlarm: (Alarm) -> Unit,
closeDialog: () -> Unit,
) {
val openDialog = viewState.showCustomDialog
val time = rememberSaveable { mutableStateOf(15) }
val units = rememberSaveable { mutableStateOf(0) }
val openRecurringDialog = rememberSaveable { mutableStateOf(false) }
val interval = rememberSaveable { mutableStateOf(0) }
val recurringUnits = rememberSaveable { mutableStateOf(0) }
val repeat = rememberSaveable { mutableStateOf(0) }
if (openDialog) {
if (!openRecurringDialog.value) {
AlertDialog(
onDismissRequest = closeDialog,
text = {
AddCustomReminder(
time,
units,
interval,
recurringUnits,
repeat,
showRecurring = {
openRecurringDialog.value = true
}
)
},
confirmButton = {
Constants.TextButton(text = R.string.ok, onClick = {
time.value.takeIf { it >= 0 }?.let { i ->
addAlarm(
Alarm(
time = -1 * i * units.millis,
type = TYPE_REL_END,
repeat = repeat.value,
interval = interval.value * recurringUnits.millis
)
)
closeDialog()
}
})
},
dismissButton = {
Constants.TextButton(
text = R.string.cancel,
onClick = closeDialog
)
},
// Create working copy from alarm or use defaults
var workingCopy by rememberSaveable {
mutableStateOf(
alarm ?: Alarm(
time = -1 * 15 * TimeUnit.MINUTES.toMillis(1),
type = TYPE_REL_END
)
}
AddRepeatReminderDialog(
openDialog = openRecurringDialog,
initialInterval = interval.value,
initialUnits = recurringUnits.value,
initialRepeat = repeat.value,
selected = { i, u, r ->
interval.value = i
recurringUnits.value = u
repeat.value = r
}
)
} else {
time.value = 15
units.value = 0
interval.value = 0
recurringUnits.value = 0
repeat.value = 0
}
}
var showRecurringDialog by rememberSaveable { mutableStateOf(false) }
@Composable
fun AddRepeatReminderDialog(
openDialog: MutableState<Boolean>,
initialInterval: Int,
initialUnits: Int,
initialRepeat: Int,
selected: (Int, Int, Int) -> Unit,
) {
val interval = rememberSaveable { mutableStateOf(initialInterval) }
val units = rememberSaveable { mutableStateOf(initialUnits) }
val repeat = rememberSaveable { mutableStateOf(initialRepeat) }
val closeDialog = {
openDialog.value = false
}
if (openDialog.value) {
if (!showRecurringDialog) {
AlertDialog(
onDismissRequest = closeDialog,
text = {
AddRecurringReminder(
openDialog.value,
interval,
units,
repeat,
AddCustomReminder(
alarm = workingCopy,
updateAlarm = { workingCopy = it },
showRecurring = { showRecurringDialog = true }
)
},
confirmButton = {
Constants.TextButton(text = R.string.ok, onClick = {
if (interval.value > 0 && repeat.value > 0) {
selected(interval.value, units.value, repeat.value)
openDialog.value = false
val (amount, _) = timeToAmountAndUnit(workingCopy.time)
if (amount >= 0) {
updateAlarm(workingCopy)
closeDialog()
}
})
},
@ -207,19 +166,74 @@ object AddReminderDialog {
)
},
)
} else {
interval.value = initialInterval.takeIf { it > 0 } ?: 15
units.value = initialUnits
repeat.value = initialRepeat.takeIf { it > 0 } ?: 4
}
if (showRecurringDialog) {
AddRepeatReminderDialog(
alarm = workingCopy,
updateAlarm = { workingCopy = it },
closeDialog = { showRecurringDialog = false }
)
}
}
@Composable
fun AddRepeatReminderDialog(
alarm: Alarm,
updateAlarm: (Alarm) -> Unit,
closeDialog: () -> Unit,
) {
// Create working copy with defaults if no recurrence set
var workingCopy by rememberSaveable {
mutableStateOf(
if (alarm.interval == 0L && alarm.repeat == 0) {
// Default to 15 minutes, 4 times
alarm.copy(
interval = 15 * TimeUnit.MINUTES.toMillis(1),
repeat = 4
)
} else {
alarm
}
)
}
AlertDialog(
onDismissRequest = closeDialog,
text = {
AddRecurringReminder(
alarm = workingCopy,
updateAlarm = { workingCopy = it }
)
},
confirmButton = {
Constants.TextButton(text = R.string.ok, onClick = {
val (intervalAmount, _) = timeToAmountAndUnit(workingCopy.interval)
if (intervalAmount > 0 && workingCopy.repeat > 0) {
updateAlarm(workingCopy)
closeDialog()
}
})
},
dismissButton = {
Constants.TextButton(
text = R.string.cancel,
onClick = closeDialog
)
},
)
}
@Composable
fun AddRandomReminder(
time: MutableState<Int>,
units: MutableState<Int>,
alarm: Alarm,
updateAlarm: (Alarm) -> Unit,
) {
val (initialAmount, initialUnit) = timeToAmountAndUnit(alarm.time)
var selectedUnit by rememberSaveable { mutableStateOf(initialUnit) }
val amount = if (alarm.time == 0L) 0 else (alarm.time / unitIndexToMillis(selectedUnit)).toInt()
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxWidth()
@ -228,14 +242,27 @@ object AddReminderDialog {
CenteredH6(text = stringResource(id = R.string.randomly_every, "").trim())
val focusRequester = remember { FocusRequester() }
OutlinedIntInput(
time,
value = amount,
onValueChange = { newAmount ->
val amt = newAmount ?: 0
updateAlarm(alarm.copy(time = amt * unitIndexToMillis(selectedUnit)))
},
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester)
)
Spacer(modifier = Modifier.height(16.dp))
options.forEachIndexed { index, option ->
RadioRow(index, option, time, units)
RadioRow(
index = index,
option = option,
timeAmount = amount,
unitIndex = selectedUnit,
onUnitSelected = { newUnit ->
selectedUnit = newUnit
updateAlarm(alarm.copy(time = amount * unitIndexToMillis(newUnit)))
}
)
}
ShowKeyboard(true, focusRequester)
}
@ -243,14 +270,19 @@ object AddReminderDialog {
@Composable
fun AddCustomReminder(
time: MutableState<Int>,
units: MutableState<Int>,
interval: MutableState<Int>,
recurringUnits: MutableState<Int>,
repeat: MutableState<Int>,
alarm: Alarm,
updateAlarm: (Alarm) -> Unit,
showRecurring: () -> Unit,
) {
val (initialAmount, initialUnit) = timeToAmountAndUnit(alarm.time)
var selectedUnit by rememberSaveable { mutableStateOf(initialUnit) }
val amount = if (alarm.time == 0L) 0 else kotlin.math.abs(alarm.time / unitIndexToMillis(selectedUnit)).toInt()
val (initialIntervalAmount, initialIntervalUnit) = timeToAmountAndUnit(alarm.interval)
val intervalAmount = if (alarm.interval == 0L) 0 else (alarm.interval / unitIndexToMillis(initialIntervalUnit)).toInt()
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxWidth()
@ -259,7 +291,11 @@ object AddReminderDialog {
CenteredH6(resId = R.string.custom_notification)
val focusRequester = remember { FocusRequester() }
OutlinedIntInput(
time,
value = amount,
onValueChange = { newAmount ->
val amt = newAmount ?: 0
updateAlarm(alarm.copy(time = -1 * amt * unitIndexToMillis(selectedUnit)))
},
minValue = 0,
modifier = Modifier
.fillMaxWidth()
@ -267,7 +303,17 @@ object AddReminderDialog {
)
Spacer(modifier = Modifier.height(16.dp))
options.forEachIndexed { index, option ->
RadioRow(index, option, time, units, R.string.alarm_before_due)
RadioRow(
index = index,
option = option,
timeAmount = amount,
unitIndex = selectedUnit,
onUnitSelected = { newUnit ->
selectedUnit = newUnit
updateAlarm(alarm.copy(time = -1 * amount * unitIndexToMillis(newUnit)))
},
formatString = R.string.alarm_before_due
)
}
Divider(modifier = Modifier.padding(vertical = 4.dp), thickness = 1.dp)
Row(modifier = Modifier
@ -288,11 +334,11 @@ object AddReminderDialog {
),
)
}
val repeating = repeat.value > 0 && interval.value > 0
val repeating = alarm.repeat > 0 && intervalAmount > 0
val text = if (repeating) {
LocalContext.current.resources.getRepeatString(
repeat.value,
interval.value * recurringUnits.millis
alarm.repeat,
alarm.interval
)
} else {
stringResource(id = R.string.repeat_option_does_not_repeat)
@ -305,11 +351,9 @@ object AddReminderDialog {
.align(CenterVertically)
)
if (repeating) {
ClearButton {
repeat.value = 0
interval.value = 0
recurringUnits.value = 0
}
ClearButton(onClick = {
updateAlarm(alarm.copy(repeat = 0, interval = 0))
})
}
}
ShowKeyboard(true, focusRequester)
@ -318,12 +362,14 @@ object AddReminderDialog {
@Composable
fun AddRecurringReminder(
openDialog: Boolean,
interval: MutableState<Int>,
units: MutableState<Int>,
repeat: MutableState<Int>
alarm: Alarm,
updateAlarm: (Alarm) -> Unit,
) {
val (initialIntervalAmount, initialIntervalUnit) = timeToAmountAndUnit(alarm.interval)
var selectedUnit by rememberSaveable { mutableStateOf(initialIntervalUnit) }
val intervalAmount = if (alarm.interval == 0L) 0 else (alarm.interval / unitIndexToMillis(selectedUnit)).toInt()
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.fillMaxWidth()
@ -332,24 +378,40 @@ object AddReminderDialog {
CenteredH6(text = stringResource(id = R.string.repeats_plural, "").trim())
val focusRequester = remember { FocusRequester() }
OutlinedIntInput(
time = interval,
value = intervalAmount,
onValueChange = { newAmount ->
val amt = newAmount ?: 0
updateAlarm(alarm.copy(interval = amt * unitIndexToMillis(selectedUnit)))
},
modifier = Modifier.focusRequester(focusRequester),
)
Spacer(modifier = Modifier.height(16.dp))
options.forEachIndexed { index, option ->
RadioRow(index, option, interval, units)
RadioRow(
index = index,
option = option,
timeAmount = intervalAmount,
unitIndex = selectedUnit,
onUnitSelected = { newUnit ->
selectedUnit = newUnit
updateAlarm(alarm.copy(interval = intervalAmount * unitIndexToMillis(newUnit)))
}
)
}
Divider(modifier = Modifier.padding(vertical = 4.dp), thickness = 1.dp)
Row(modifier = Modifier.fillMaxWidth()) {
OutlinedIntInput(
time = repeat,
value = alarm.repeat,
onValueChange = { newRepeat ->
updateAlarm(alarm.copy(repeat = newRepeat ?: 0))
},
modifier = Modifier.weight(0.5f),
autoSelect = false,
)
BodyText(
text = LocalContext.current.resources.getQuantityString(
R.plurals.repeat_times,
repeat.value
alarm.repeat
),
modifier = Modifier
.weight(0.5f)
@ -357,7 +419,7 @@ object AddReminderDialog {
)
}
ShowKeyboard(openDialog, focusRequester)
ShowKeyboard(true, focusRequester)
}
}
@ -367,14 +429,6 @@ object AddReminderDialog {
R.plurals.reminder_days,
R.plurals.reminder_week,
)
private val MutableState<Int>.millis: Long
get() = when (value) {
1 -> TimeUnit.HOURS.toMillis(1)
2 -> TimeUnit.DAYS.toMillis(1)
3 -> TimeUnit.DAYS.toMillis(7)
else -> TimeUnit.MINUTES.toMillis(1)
}
}
@ExperimentalComposeUiApi
@ -391,25 +445,48 @@ fun ShowKeyboard(visible: Boolean, focusRequester: FocusRequester) {
@Composable
fun OutlinedIntInput(
time: MutableState<Int>,
value: Int?,
onValueChange: (Int?) -> Unit,
modifier: Modifier = Modifier,
minValue: Int = 1,
autoSelect: Boolean = true,
) {
val value = rememberSaveable(stateSaver = TextFieldValue.Saver) {
val text = time.value.toString()
var textFieldValue by remember {
mutableStateOf(
TextFieldValue(
text = text,
selection = TextRange(0, if (autoSelect) text.length else 0)
text = value?.toString() ?: "",
selection = if (autoSelect) {
TextRange(0, value?.toString()?.length ?: 0)
} else {
TextRange.Zero
}
)
)
}
// Sync when external value changes, but don't interfere with user editing
LaunchedEffect(value) {
val currentParsedValue = textFieldValue.text.toIntOrNull()
// Only sync if the new value is different from what we currently parse to,
// and don't sync if the text field is empty (user is actively deleting)
if (currentParsedValue != value && textFieldValue.text.isNotEmpty()) {
val newText = value?.toString() ?: ""
textFieldValue = TextFieldValue(
text = newText,
selection = if (autoSelect) {
TextRange(0, newText.length)
} else {
textFieldValue.selection
}
)
}
}
OutlinedTextField(
value = value.value,
value = textFieldValue,
onValueChange = {
value.value = it.copy(text = it.text.filter { t -> t.isDigit() })
time.value = value.value.text.toIntOrNull() ?: 0
textFieldValue = it.copy(text = it.text.filter { t -> t.isDigit() })
onValueChange(textFieldValue.text.toIntOrNull())
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = modifier.padding(horizontal = 16.dp),
@ -419,7 +496,7 @@ fun OutlinedIntInput(
focusedBorderColor = MaterialTheme.colorScheme.onSurface,
unfocusedBorderColor = MaterialTheme.colorScheme.onSurface,
),
isError = value.value.text.toIntOrNull()?.let { it < minValue } ?: true,
isError = textFieldValue.text.toIntOrNull()?.let { it < minValue } ?: true,
)
}
@ -445,23 +522,24 @@ fun CenteredH6(text: String) {
fun RadioRow(
index: Int,
option: Int,
time: MutableState<Int>,
units: MutableState<Int>,
timeAmount: Int,
unitIndex: Int,
onUnitSelected: (Int) -> Unit,
formatString: Int? = null,
) {
val optionString = LocalContext.current.resources.getQuantityString(option, time.value)
val optionString = LocalContext.current.resources.getQuantityString(option, timeAmount)
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { units.value = index }
.clickable { onUnitSelected(index) }
) {
RadioButton(
selected = index == units.value,
onClick = { units.value = index },
selected = index == unitIndex,
onClick = { onUnitSelected(index) },
modifier = Modifier.align(CenterVertically)
)
BodyText(
text = if (index == units.value) {
text = if (index == unitIndex) {
formatString
?.let { stringResource(id = formatString, optionString) }
?: optionString
@ -506,8 +584,14 @@ fun AddAlarmDialog(
dismiss()
return
}
// TODO: if replacing custom alarm show custom picker
// TODO: prepopulate pickers with existing values
TYPE_REL_END -> {
if (viewState.replace.time < 0) {
// Custom reminder (before due)
addCustom()
dismiss()
return
}
}
}
}
CustomDialog(visible = viewState.showAddAlarm, onDismiss = dismiss) {
@ -555,11 +639,11 @@ fun AddAlarmDialog(
fun AddCustomReminderOne() =
TasksTheme {
AddReminderDialog.AddCustomReminder(
time = remember { mutableStateOf(1) },
units = remember { mutableStateOf(0) },
interval = remember { mutableStateOf(0) },
recurringUnits = remember { mutableStateOf(0) },
repeat = remember { mutableStateOf(0) },
alarm = Alarm(
time = -1 * TimeUnit.MINUTES.toMillis(1),
type = TYPE_REL_END
),
updateAlarm = {},
showRecurring = {},
)
}
@ -571,11 +655,11 @@ fun AddCustomReminderOne() =
fun AddCustomReminder() =
TasksTheme {
AddReminderDialog.AddCustomReminder(
time = remember { mutableStateOf(15) },
units = remember { mutableStateOf(1) },
interval = remember { mutableStateOf(0) },
recurringUnits = remember { mutableStateOf(0) },
repeat = remember { mutableStateOf(0) },
alarm = Alarm(
time = -15 * TimeUnit.HOURS.toMillis(1),
type = TYPE_REL_END
),
updateAlarm = {},
showRecurring = {},
)
}
@ -587,10 +671,13 @@ fun AddCustomReminder() =
fun AddRepeatingReminderOne() =
TasksTheme {
AddReminderDialog.AddRecurringReminder(
openDialog = true,
interval = remember { mutableStateOf(1) },
units = remember { mutableStateOf(0) },
repeat = remember { mutableStateOf(1) },
alarm = Alarm(
time = -1 * TimeUnit.MINUTES.toMillis(1),
type = TYPE_REL_END,
interval = TimeUnit.MINUTES.toMillis(1),
repeat = 1
),
updateAlarm = {},
)
}
@ -601,10 +688,13 @@ fun AddRepeatingReminderOne() =
fun AddRepeatingReminder() =
TasksTheme {
AddReminderDialog.AddRecurringReminder(
openDialog = true,
interval = remember { mutableStateOf(15) },
units = remember { mutableStateOf(1) },
repeat = remember { mutableStateOf(4) },
alarm = Alarm(
time = -15 * TimeUnit.HOURS.toMillis(1),
type = TYPE_REL_END,
interval = 15 * TimeUnit.HOURS.toMillis(1),
repeat = 4
),
updateAlarm = {},
)
}
@ -615,8 +705,11 @@ fun AddRepeatingReminder() =
fun AddRandomReminderOne() =
TasksTheme {
AddReminderDialog.AddRandomReminder(
time = remember { mutableStateOf(1) },
units = remember { mutableStateOf(0) }
alarm = Alarm(
time = TimeUnit.MINUTES.toMillis(1),
type = TYPE_RANDOM
),
updateAlarm = {}
)
}
@ -627,8 +720,11 @@ fun AddRandomReminderOne() =
fun AddRandomReminder() =
TasksTheme {
AddReminderDialog.AddRandomReminder(
time = remember { mutableStateOf(15) },
units = remember { mutableStateOf(1) }
alarm = Alarm(
time = 15 * TimeUnit.HOURS.toMillis(1),
type = TYPE_RANDOM
),
updateAlarm = {}
)
}

@ -106,23 +106,27 @@ fun AlarmRow(
dismiss = { vm.showAddAlarm(visible = false) },
)
AddReminderDialog.AddCustomReminderDialog(
viewState = viewState,
addAlarm = {
viewState.replace?.let(deleteAlarm)
addAlarm(it)
},
closeDialog = { vm.showCustomDialog(visible = false) }
)
if (viewState.showCustomDialog) {
AddReminderDialog.AddCustomReminderDialog(
alarm = viewState.replace,
updateAlarm = {
viewState.replace?.let(deleteAlarm)
addAlarm(it)
},
closeDialog = { vm.showCustomDialog(visible = false) }
)
}
AddReminderDialog.AddRandomReminderDialog(
viewState = viewState,
addAlarm = {
viewState.replace?.let(deleteAlarm)
addAlarm(it)
},
closeDialog = { vm.showRandomDialog(visible = false) }
)
if (viewState.showRandomDialog) {
AddReminderDialog.AddRandomReminderDialog(
alarm = viewState.replace,
updateAlarm = {
viewState.replace?.let(deleteAlarm)
addAlarm(it)
},
closeDialog = { vm.showRandomDialog(visible = false) }
)
}
},
)
}

Loading…
Cancel
Save