@ -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. MutableStat e
import androidx.compose.runtime. getValu e
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 ? ,
update Alarm: ( 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 ? ,
update Alarm: ( 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 ,
u pdateAlarm: ( 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 = textFieldV alue,
onValueChange = {
value . value = it . copy ( text = it . text . filter { t -> t . isDigit ( ) } )
time . value = value . value . text . toIntOrNull ( ) ?: 0
textFieldV alue = 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 . v alue. text . toIntOrNull ( ) ?. let { it < minValue } ?: true ,
isError = textFieldV alue. 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 , time Amount )
Row (
modifier = Modifier
. fillMaxWidth ( )
. clickable { units. value = index }
. clickable { onUnitSelected( index ) }
) {
RadioButton (
selected = index == unit s. value ,
onClick = { units. value = index } ,
selected = index == unit Index ,
onClick = { onUnitSelected( index ) } ,
modifier = Modifier . align ( CenterVertically )
)
BodyText (
text = if ( index == unit s. value ) {
text = if ( index == unit Index ) {
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 = { }
)
}