mirror of https://github.com/tasks/tasks
New recurrence activity
parent
5308404ed6
commit
dff522437d
@ -0,0 +1,31 @@
|
|||||||
|
package org.tasks.compose
|
||||||
|
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun border() = MaterialTheme.colors.onSurface.copy(alpha = .5f)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun OutlinedBox(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable BoxScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.height(45.dp)
|
||||||
|
.border(1.dp, color = border(), RoundedCornerShape(4.dp))
|
||||||
|
.padding(start = 8.dp, end = 8.dp),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
content = content,
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
package org.tasks.compose
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.DropdownMenu
|
||||||
|
import androidx.compose.material.DropdownMenuItem
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.ArrowDropDown
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.DpOffset
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun OutlinedSpinner(
|
||||||
|
text: String,
|
||||||
|
options: List<String>,
|
||||||
|
onSelected: (Int) -> Unit,
|
||||||
|
) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
OutlinedBox(
|
||||||
|
modifier = Modifier.clickable { expanded = !expanded }
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.padding(start = 8.dp),
|
||||||
|
) {
|
||||||
|
Text(text = text)
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.ArrowDropDown,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colors.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = { expanded = false },
|
||||||
|
offset = DpOffset(-8.dp, 0.dp),
|
||||||
|
) {
|
||||||
|
options.forEachIndexed { index, item ->
|
||||||
|
DropdownMenuItem(
|
||||||
|
onClick = {
|
||||||
|
expanded = false
|
||||||
|
onSelected(index)
|
||||||
|
},
|
||||||
|
content = {
|
||||||
|
Text(
|
||||||
|
text = item,
|
||||||
|
style = MaterialTheme.typography.body2,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,91 @@
|
|||||||
|
package org.tasks.compose
|
||||||
|
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.TextFieldDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.os.ConfigurationCompat
|
||||||
|
import org.tasks.extensions.formatNumber
|
||||||
|
import org.tasks.extensions.parseInteger
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
|
@Composable
|
||||||
|
fun OutlinedNumberInput(
|
||||||
|
number: Int,
|
||||||
|
onTextChanged: (Int) -> Unit,
|
||||||
|
onFocus: () -> Unit = {},
|
||||||
|
) {
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val context = LocalContext.current
|
||||||
|
val locale = remember {
|
||||||
|
ConfigurationCompat
|
||||||
|
.getLocales(context.resources.configuration)
|
||||||
|
.get(0)
|
||||||
|
?: Locale.getDefault()
|
||||||
|
}
|
||||||
|
val numberString = remember(number) {
|
||||||
|
number.takeIf { it > 0 }?.let { locale.formatNumber(it) } ?: ""
|
||||||
|
}
|
||||||
|
BasicTextField(
|
||||||
|
value = numberString,
|
||||||
|
onValueChange = {
|
||||||
|
val newValue = locale
|
||||||
|
.parseInteger(it)
|
||||||
|
?: 0
|
||||||
|
onTextChanged(newValue)
|
||||||
|
},
|
||||||
|
textStyle = MaterialTheme.typography.body1.copy(
|
||||||
|
color = MaterialTheme.colors.onSurface,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.border(
|
||||||
|
width = 1.dp,
|
||||||
|
color = border(),
|
||||||
|
shape = RoundedCornerShape(4.dp),
|
||||||
|
)
|
||||||
|
.onFocusChanged {
|
||||||
|
if (it.hasFocus) {
|
||||||
|
onFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.width(60.dp)
|
||||||
|
.height(45.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
cursorBrush = SolidColor(MaterialTheme.colors.onBackground),
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
singleLine = true,
|
||||||
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
|
) {
|
||||||
|
TextFieldDefaults.TextFieldDecorationBox(
|
||||||
|
value = number.toString(),
|
||||||
|
innerTextField = it,
|
||||||
|
singleLine = true,
|
||||||
|
enabled = true,
|
||||||
|
visualTransformation = VisualTransformation.None,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
// keep horizontal paddings but change the vertical
|
||||||
|
contentPadding = TextFieldDefaults.textFieldWithoutLabelPadding(
|
||||||
|
top = 0.dp, bottom = 0.dp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,526 @@
|
|||||||
|
package org.tasks.compose.pickers
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.os.LocaleList
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.IconButton
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.material.RadioButton
|
||||||
|
import androidx.compose.material.Surface
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material.TopAppBar
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.ArrowBack
|
||||||
|
import androidx.compose.material3.Divider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.os.ConfigurationCompat
|
||||||
|
import com.google.android.material.composethemeadapter.MdcTheme
|
||||||
|
import com.todoroo.andlib.utility.DateUtilities
|
||||||
|
import net.fortuna.ical4j.model.Recur
|
||||||
|
import net.fortuna.ical4j.model.WeekDay
|
||||||
|
import org.tasks.R
|
||||||
|
import org.tasks.compose.OutlinedBox
|
||||||
|
import org.tasks.compose.OutlinedNumberInput
|
||||||
|
import org.tasks.compose.OutlinedSpinner
|
||||||
|
import org.tasks.compose.border
|
||||||
|
import org.tasks.repeats.CustomRecurrenceViewModel
|
||||||
|
import java.time.DayOfWeek
|
||||||
|
import java.time.format.FormatStyle
|
||||||
|
import java.time.format.TextStyle
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CustomRecurrence(
|
||||||
|
state: CustomRecurrenceViewModel.ViewState,
|
||||||
|
save: () -> Unit,
|
||||||
|
discard: () -> Unit,
|
||||||
|
setInterval: (Int) -> Unit,
|
||||||
|
setSelectedFrequency: (Recur.Frequency) -> Unit,
|
||||||
|
setEndDate: (Long) -> Unit,
|
||||||
|
setSelectedEndType: (Int) -> Unit,
|
||||||
|
setOccurrences: (Int) -> Unit,
|
||||||
|
toggleDay: (DayOfWeek) -> Unit,
|
||||||
|
setMonthSelection: (Int) -> Unit,
|
||||||
|
) {
|
||||||
|
BackHandler {
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.repeats_custom_recurrence),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = save) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.ArrowBack,
|
||||||
|
contentDescription = stringResource(id = R.string.save),
|
||||||
|
tint = MaterialTheme.colors.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
TextButton(onClick = discard) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.cancel),
|
||||||
|
style = MaterialTheme.typography.body1.copy(
|
||||||
|
fontFeatureSettings = "c2sc, smcp"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
backgroundColor = MaterialTheme.colors.surface,
|
||||||
|
contentColor = MaterialTheme.colors.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { padding ->
|
||||||
|
Surface(
|
||||||
|
color = MaterialTheme.colors.surface,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding),
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Header(R.string.repeats_every)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
) {
|
||||||
|
OutlinedNumberInput(
|
||||||
|
number = state.interval,
|
||||||
|
onTextChanged = setInterval,
|
||||||
|
)
|
||||||
|
val context = LocalContext.current
|
||||||
|
val options by remember(state.interval, state.frequency) {
|
||||||
|
derivedStateOf {
|
||||||
|
state.frequencyOptions.map {
|
||||||
|
context.resources.getQuantityString(
|
||||||
|
it.plural,
|
||||||
|
state.interval,
|
||||||
|
state.interval,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OutlinedSpinner(
|
||||||
|
text = pluralStringResource(
|
||||||
|
id = state.frequency.plural,
|
||||||
|
count = state.interval
|
||||||
|
),
|
||||||
|
options = options,
|
||||||
|
onSelected = { setSelectedFrequency(state.frequencyOptions[it]) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (state.frequency == Recur.Frequency.WEEKLY) {
|
||||||
|
WeekdayPicker(
|
||||||
|
daysOfWeek = state.daysOfWeek,
|
||||||
|
selected = state.selectedDays,
|
||||||
|
toggle = toggleDay,
|
||||||
|
)
|
||||||
|
} else if (state.frequency == Recur.Frequency.MONTHLY) {
|
||||||
|
MonthlyPicker(
|
||||||
|
monthDay = state.monthDay,
|
||||||
|
dayNumber = state.dueDayOfMonth,
|
||||||
|
dayOfWeek = state.dueDayOfWeek,
|
||||||
|
nthWeek = state.nthWeek,
|
||||||
|
isLastWeek = state.lastWeekDayOfMonth,
|
||||||
|
locale = state.locale,
|
||||||
|
onSelected = setMonthSelection,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Divider(
|
||||||
|
modifier = Modifier.padding(vertical = if (state.frequency == Recur.Frequency.WEEKLY) 11.dp else 16.dp),
|
||||||
|
color = border()
|
||||||
|
)
|
||||||
|
EndsPicker(
|
||||||
|
selection = state.endSelection,
|
||||||
|
endDate = state.endDate,
|
||||||
|
endOccurrences = state.endCount,
|
||||||
|
setEndDate = setEndDate,
|
||||||
|
setSelection = setSelectedEndType,
|
||||||
|
setOccurrences = setOccurrences,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Header(resId: Int) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = resId),
|
||||||
|
style = MaterialTheme.typography.caption,
|
||||||
|
color = MaterialTheme.colors.onSurface,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun WeekdayPicker(
|
||||||
|
daysOfWeek: List<DayOfWeek>,
|
||||||
|
selected: List<DayOfWeek>,
|
||||||
|
toggle: (DayOfWeek) -> Unit,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val locale = remember {
|
||||||
|
ConfigurationCompat
|
||||||
|
.getLocales(context.resources.configuration)
|
||||||
|
.get(0)
|
||||||
|
?: Locale.getDefault()
|
||||||
|
}
|
||||||
|
Divider(
|
||||||
|
modifier = Modifier.padding(vertical = 16.dp),
|
||||||
|
color = border()
|
||||||
|
)
|
||||||
|
Header(R.string.repeats_weekly_on)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
FlowRow(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
) {
|
||||||
|
daysOfWeek.forEach { dayOfWeek ->
|
||||||
|
val string = remember(dayOfWeek) {
|
||||||
|
dayOfWeek.getDisplayName(TextStyle.NARROW, locale)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(bottom = 5.dp) // hack until compose 1.5
|
||||||
|
.size(36.dp)
|
||||||
|
.let {
|
||||||
|
if (selected.contains(dayOfWeek)) {
|
||||||
|
it.background(MaterialTheme.colors.secondary, shape = CircleShape)
|
||||||
|
} else {
|
||||||
|
it.border(1.dp, border(), shape = CircleShape)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.clickable { toggle(dayOfWeek) },
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = string,
|
||||||
|
style = MaterialTheme.typography.body2,
|
||||||
|
color = if (selected.contains(dayOfWeek)) MaterialTheme.colors.onSecondary else MaterialTheme.colors.onSurface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MonthlyPicker(
|
||||||
|
monthDay: WeekDay?,
|
||||||
|
dayNumber: Int,
|
||||||
|
dayOfWeek: DayOfWeek,
|
||||||
|
nthWeek: Int,
|
||||||
|
isLastWeek: Boolean,
|
||||||
|
locale: Locale,
|
||||||
|
onSelected: (Int) -> Unit,
|
||||||
|
) {
|
||||||
|
val selection = remember(monthDay) {
|
||||||
|
when (monthDay?.offset) {
|
||||||
|
null -> 0
|
||||||
|
-1 -> 2
|
||||||
|
else -> 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Divider(
|
||||||
|
modifier = Modifier.padding(vertical = 16.dp),
|
||||||
|
color = border()
|
||||||
|
)
|
||||||
|
val context = LocalContext.current
|
||||||
|
val options = remember(dayNumber, dayOfWeek, nthWeek, isLastWeek, locale) {
|
||||||
|
ArrayList<String>().apply {
|
||||||
|
add(context.getString(R.string.repeat_monthly_on_day_number, dayNumber))
|
||||||
|
val nth = context.getString(
|
||||||
|
when (nthWeek - 1) {
|
||||||
|
0 -> R.string.repeat_monthly_first_week
|
||||||
|
1 -> R.string.repeat_monthly_second_week
|
||||||
|
2 -> R.string.repeat_monthly_third_week
|
||||||
|
3 -> R.string.repeat_monthly_fourth_week
|
||||||
|
4 -> R.string.repeat_monthly_fifth_week
|
||||||
|
else -> throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val dayOfWeekDisplayName = dayOfWeek.getDisplayName(TextStyle.FULL, locale)
|
||||||
|
add(
|
||||||
|
context.getString(
|
||||||
|
R.string.repeat_monthly_on_the_nth_weekday,
|
||||||
|
nth,
|
||||||
|
dayOfWeekDisplayName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (isLastWeek) {
|
||||||
|
add(
|
||||||
|
context.getString(
|
||||||
|
R.string.repeat_monthly_on_the_nth_weekday,
|
||||||
|
context.getString(R.string.repeat_monthly_last_week),
|
||||||
|
dayOfWeekDisplayName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
) {
|
||||||
|
OutlinedSpinner(
|
||||||
|
text = options[selection],
|
||||||
|
options = options,
|
||||||
|
onSelected = onSelected,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EndsPicker(
|
||||||
|
selection: Int,
|
||||||
|
endDate: Long,
|
||||||
|
endOccurrences: Int,
|
||||||
|
setOccurrences: (Int) -> Unit,
|
||||||
|
setEndDate: (Long) -> Unit,
|
||||||
|
setSelection: (Int) -> Unit,
|
||||||
|
) {
|
||||||
|
Header(R.string.repeats_ends)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
RadioRow(selected = selection == 0, onClick = { setSelection(0) }) {
|
||||||
|
Text(text = stringResource(id = R.string.repeats_never))
|
||||||
|
}
|
||||||
|
Divider(
|
||||||
|
modifier = Modifier.padding(start = 50.dp, end = 16.dp, top = 8.dp, bottom = 8.dp),
|
||||||
|
color = border()
|
||||||
|
)
|
||||||
|
RadioRow(selected = selection == 1, onClick = { setSelection(1) }) {
|
||||||
|
Text(text = stringResource(id = R.string.repeats_on))
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
val context = LocalContext.current
|
||||||
|
val locale = remember { LocaleList.getDefault()[0] }
|
||||||
|
val endDateString by remember(context, endDate) {
|
||||||
|
derivedStateOf {
|
||||||
|
DateUtilities.getRelativeDay(context, endDate, locale, FormatStyle.MEDIUM)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var showDatePicker by remember { mutableStateOf(false) }
|
||||||
|
if (showDatePicker) {
|
||||||
|
DatePickerDialog(
|
||||||
|
initialDate = endDate,
|
||||||
|
selected = { setEndDate(it) },
|
||||||
|
dismiss = { showDatePicker = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
OutlinedBox(
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
setSelection(1)
|
||||||
|
showDatePicker = true
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(text = endDateString)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Divider(
|
||||||
|
modifier = Modifier.padding(start = 50.dp, end = 16.dp, top = 8.dp, bottom = 8.dp),
|
||||||
|
color = border()
|
||||||
|
)
|
||||||
|
RadioRow(selected = selection == 2, onClick = { setSelection(2) }) {
|
||||||
|
Text(text = stringResource(id = R.string.repeats_after))
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
OutlinedNumberInput(
|
||||||
|
number = endOccurrences,
|
||||||
|
onTextChanged = setOccurrences,
|
||||||
|
onFocus = { setSelection(2) },
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Text(text = pluralStringResource(id = R.plurals.repeat_occurrence, endOccurrences))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RadioRow(
|
||||||
|
selected: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
content: @Composable RowScope.() -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { onClick() },
|
||||||
|
) {
|
||||||
|
RadioButton(selected = selected, onClick = onClick)
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
content = content,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val Recur.Frequency.plural: Int
|
||||||
|
get() = when (this) {
|
||||||
|
Recur.Frequency.MINUTELY -> R.plurals.repeat_minutes
|
||||||
|
Recur.Frequency.HOURLY -> R.plurals.repeat_hours
|
||||||
|
Recur.Frequency.DAILY -> R.plurals.repeat_days
|
||||||
|
Recur.Frequency.WEEKLY -> R.plurals.repeat_weeks
|
||||||
|
Recur.Frequency.MONTHLY -> R.plurals.repeat_months
|
||||||
|
Recur.Frequency.YEARLY -> R.plurals.repeat_years
|
||||||
|
else -> throw RuntimeException()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
fun WeeklyPreview() {
|
||||||
|
MdcTheme {
|
||||||
|
CustomRecurrence(
|
||||||
|
state = CustomRecurrenceViewModel.ViewState(frequency = Recur.Frequency.WEEKLY),
|
||||||
|
save = {},
|
||||||
|
discard = {},
|
||||||
|
setSelectedFrequency = {},
|
||||||
|
setSelectedEndType = {},
|
||||||
|
setEndDate = {},
|
||||||
|
setInterval = {},
|
||||||
|
setOccurrences = {},
|
||||||
|
toggleDay = {},
|
||||||
|
setMonthSelection = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
fun MonthlyPreview() {
|
||||||
|
MdcTheme {
|
||||||
|
CustomRecurrence(
|
||||||
|
state = CustomRecurrenceViewModel.ViewState(frequency = Recur.Frequency.MONTHLY),
|
||||||
|
save = {},
|
||||||
|
discard = {},
|
||||||
|
setSelectedFrequency = {},
|
||||||
|
setSelectedEndType = {},
|
||||||
|
setEndDate = {},
|
||||||
|
setInterval = {},
|
||||||
|
setOccurrences = {},
|
||||||
|
toggleDay = {},
|
||||||
|
setMonthSelection = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
fun MinutelyPreview() {
|
||||||
|
MdcTheme {
|
||||||
|
CustomRecurrence(
|
||||||
|
state = CustomRecurrenceViewModel.ViewState(frequency = Recur.Frequency.MINUTELY),
|
||||||
|
save = {},
|
||||||
|
discard = {},
|
||||||
|
setSelectedFrequency = {},
|
||||||
|
setSelectedEndType = {},
|
||||||
|
setEndDate = {},
|
||||||
|
setInterval = {},
|
||||||
|
setOccurrences = {},
|
||||||
|
toggleDay = {},
|
||||||
|
setMonthSelection = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
fun HourlyPreview() {
|
||||||
|
MdcTheme {
|
||||||
|
CustomRecurrence(
|
||||||
|
state = CustomRecurrenceViewModel.ViewState(frequency = Recur.Frequency.HOURLY),
|
||||||
|
save = {},
|
||||||
|
discard = {},
|
||||||
|
setSelectedFrequency = {},
|
||||||
|
setSelectedEndType = {},
|
||||||
|
setEndDate = {},
|
||||||
|
setInterval = {},
|
||||||
|
setOccurrences = {},
|
||||||
|
toggleDay = {},
|
||||||
|
setMonthSelection = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
fun DailyPreview() {
|
||||||
|
MdcTheme {
|
||||||
|
CustomRecurrence(
|
||||||
|
state = CustomRecurrenceViewModel.ViewState(frequency = Recur.Frequency.DAILY),
|
||||||
|
save = {},
|
||||||
|
discard = {},
|
||||||
|
setSelectedFrequency = {},
|
||||||
|
setSelectedEndType = {},
|
||||||
|
setEndDate = {},
|
||||||
|
setInterval = {},
|
||||||
|
setOccurrences = {},
|
||||||
|
toggleDay = {},
|
||||||
|
setMonthSelection = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
fun YearlyPreview() {
|
||||||
|
MdcTheme {
|
||||||
|
CustomRecurrence(
|
||||||
|
state = CustomRecurrenceViewModel.ViewState(frequency = Recur.Frequency.YEARLY),
|
||||||
|
save = {},
|
||||||
|
discard = {},
|
||||||
|
setSelectedFrequency = {},
|
||||||
|
setSelectedEndType = {},
|
||||||
|
setEndDate = {},
|
||||||
|
setInterval = {},
|
||||||
|
setOccurrences = {},
|
||||||
|
toggleDay = {},
|
||||||
|
setMonthSelection = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,77 @@
|
|||||||
|
package org.tasks.compose.pickers
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.material.Text
|
||||||
|
import androidx.compose.material3.DatePicker
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.darkColorScheme
|
||||||
|
import androidx.compose.material3.lightColorScheme
|
||||||
|
import androidx.compose.material3.rememberDatePickerState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import com.google.android.material.composethemeadapter.MdcTheme
|
||||||
|
import org.tasks.R
|
||||||
|
import org.tasks.time.DateTime
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun DatePickerDialog(
|
||||||
|
initialDate: Long,
|
||||||
|
selected: (Long) -> Unit,
|
||||||
|
dismiss: () -> Unit,
|
||||||
|
) {
|
||||||
|
MaterialTheme(
|
||||||
|
colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme()
|
||||||
|
) {
|
||||||
|
val initialDateUTC by remember(initialDate) {
|
||||||
|
derivedStateOf {
|
||||||
|
DateTime(initialDate).toUTC().millis
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val datePickerState = rememberDatePickerState(
|
||||||
|
initialSelectedDateMillis = initialDateUTC,
|
||||||
|
)
|
||||||
|
androidx.compose.material3.DatePickerDialog(
|
||||||
|
onDismissRequest = { dismiss() },
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = dismiss) {
|
||||||
|
Text(text = stringResource(id = R.string.cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
datePickerState
|
||||||
|
.selectedDateMillis
|
||||||
|
?.let { selected(it - DateTime(it).offset) }
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(id = R.string.ok))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
DatePicker(state = datePickerState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
fun DatePickerPreview() {
|
||||||
|
MdcTheme {
|
||||||
|
DatePickerDialog(
|
||||||
|
initialDate = DateTime().plusDays(1).millis,
|
||||||
|
selected = {},
|
||||||
|
dismiss = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,163 +0,0 @@
|
|||||||
package org.tasks.repeats;
|
|
||||||
|
|
||||||
import static android.app.Activity.RESULT_OK;
|
|
||||||
import static com.google.common.collect.Lists.newArrayList;
|
|
||||||
import static net.fortuna.ical4j.model.Recur.Frequency.DAILY;
|
|
||||||
import static net.fortuna.ical4j.model.Recur.Frequency.HOURLY;
|
|
||||||
import static net.fortuna.ical4j.model.Recur.Frequency.MINUTELY;
|
|
||||||
import static net.fortuna.ical4j.model.Recur.Frequency.MONTHLY;
|
|
||||||
import static net.fortuna.ical4j.model.Recur.Frequency.WEEKLY;
|
|
||||||
import static net.fortuna.ical4j.model.Recur.Frequency.YEARLY;
|
|
||||||
import static org.tasks.Strings.isNullOrEmpty;
|
|
||||||
import static org.tasks.repeats.CustomRecurrenceDialog.newCustomRecurrenceDialog;
|
|
||||||
import static org.tasks.repeats.RecurrenceUtils.newRecur;
|
|
||||||
import static org.tasks.time.DateTimeUtils.currentTimeMillis;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.Dialog;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.fragment.app.DialogFragment;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint;
|
|
||||||
import java.util.List;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import net.fortuna.ical4j.model.Recur;
|
|
||||||
import net.fortuna.ical4j.model.Recur.Frequency;
|
|
||||||
import org.tasks.R;
|
|
||||||
import org.tasks.dialogs.DialogBuilder;
|
|
||||||
import org.tasks.ui.SingleCheckedArrayAdapter;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
public class BasicRecurrenceDialog extends DialogFragment {
|
|
||||||
|
|
||||||
public static final String EXTRA_RRULE = "extra_rrule";
|
|
||||||
private static final String EXTRA_DATE = "extra_date";
|
|
||||||
private static final String FRAG_TAG_CUSTOM_RECURRENCE = "frag_tag_custom_recurrence";
|
|
||||||
|
|
||||||
@Inject Activity context;
|
|
||||||
@Inject DialogBuilder dialogBuilder;
|
|
||||||
@Inject RepeatRuleToString repeatRuleToString;
|
|
||||||
|
|
||||||
public static BasicRecurrenceDialog newBasicRecurrenceDialog(
|
|
||||||
Fragment target, int rc, String rrule, long dueDate) {
|
|
||||||
BasicRecurrenceDialog dialog = new BasicRecurrenceDialog();
|
|
||||||
dialog.setTargetFragment(target, rc);
|
|
||||||
Bundle arguments = new Bundle();
|
|
||||||
if (rrule != null) {
|
|
||||||
arguments.putString(EXTRA_RRULE, rrule);
|
|
||||||
}
|
|
||||||
arguments.putLong(EXTRA_DATE, dueDate);
|
|
||||||
dialog.setArguments(arguments);
|
|
||||||
return dialog;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
|
||||||
Bundle arguments = getArguments();
|
|
||||||
long dueDate = arguments.getLong(EXTRA_DATE, currentTimeMillis());
|
|
||||||
String rule = arguments.getString(EXTRA_RRULE);
|
|
||||||
Recur rrule = null;
|
|
||||||
try {
|
|
||||||
if (!isNullOrEmpty(rule)) {
|
|
||||||
rrule = newRecur(rule);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Timber.e(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean customPicked = isCustomValue(rrule);
|
|
||||||
List<String> repeatOptions =
|
|
||||||
newArrayList(context.getResources().getStringArray(R.array.repeat_options));
|
|
||||||
SingleCheckedArrayAdapter adapter =
|
|
||||||
new SingleCheckedArrayAdapter(context, repeatOptions);
|
|
||||||
int selected = 0;
|
|
||||||
if (customPicked) {
|
|
||||||
adapter.insert(repeatRuleToString.toString(rule), 0);
|
|
||||||
} else if (rrule != null) {
|
|
||||||
switch (rrule.getFrequency()) {
|
|
||||||
case DAILY:
|
|
||||||
selected = 1;
|
|
||||||
break;
|
|
||||||
case WEEKLY:
|
|
||||||
selected = 2;
|
|
||||||
break;
|
|
||||||
case MONTHLY:
|
|
||||||
selected = 3;
|
|
||||||
break;
|
|
||||||
case YEARLY:
|
|
||||||
selected = 4;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
selected = 0;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dialogBuilder
|
|
||||||
.newDialog()
|
|
||||||
.setSingleChoiceItems(
|
|
||||||
adapter,
|
|
||||||
selected,
|
|
||||||
(dialogInterface, i) -> {
|
|
||||||
if (customPicked) {
|
|
||||||
if (i == 0) {
|
|
||||||
dialogInterface.dismiss();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
i--;
|
|
||||||
}
|
|
||||||
Recur result;
|
|
||||||
if (i == 0) {
|
|
||||||
result = null;
|
|
||||||
} else if (i == 5) {
|
|
||||||
newCustomRecurrenceDialog(
|
|
||||||
getTargetFragment(), getTargetRequestCode(), rule, dueDate)
|
|
||||||
.show(getParentFragmentManager(), FRAG_TAG_CUSTOM_RECURRENCE);
|
|
||||||
dialogInterface.dismiss();
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
result = newRecur();
|
|
||||||
result.setInterval(1);
|
|
||||||
|
|
||||||
switch (i) {
|
|
||||||
case 1:
|
|
||||||
result.setFrequency(DAILY.name());
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
result.setFrequency(WEEKLY.name());
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
result.setFrequency(MONTHLY.name());
|
|
||||||
break;
|
|
||||||
case 4:
|
|
||||||
result.setFrequency(YEARLY.name());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Intent intent = new Intent();
|
|
||||||
intent.putExtra(EXTRA_RRULE, result == null ? null : result.toString());
|
|
||||||
getTargetFragment().onActivityResult(getTargetRequestCode(), RESULT_OK, intent);
|
|
||||||
dialogInterface.dismiss();
|
|
||||||
})
|
|
||||||
.setOnCancelListener(null)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isCustomValue(Recur rrule) {
|
|
||||||
if (rrule == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
Frequency frequency = rrule.getFrequency();
|
|
||||||
return (frequency == WEEKLY || frequency == MONTHLY) && !rrule.getDayList().isEmpty()
|
|
||||||
|| frequency == HOURLY
|
|
||||||
|| frequency == MINUTELY
|
|
||||||
|| rrule.getUntil() != null
|
|
||||||
|| rrule.getInterval() > 1
|
|
||||||
|| rrule.getCount() > 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,130 @@
|
|||||||
|
package org.tasks.repeats
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.app.Activity.RESULT_OK
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import com.google.common.collect.Lists
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import net.fortuna.ical4j.model.Recur
|
||||||
|
import org.tasks.R
|
||||||
|
import org.tasks.Strings.isNullOrEmpty
|
||||||
|
import org.tasks.dialogs.DialogBuilder
|
||||||
|
import org.tasks.repeats.CustomRecurrenceActivity.Companion.newIntent
|
||||||
|
import org.tasks.repeats.RecurrenceUtils.newRecur
|
||||||
|
import org.tasks.time.DateTimeUtils.currentTimeMillis
|
||||||
|
import org.tasks.ui.SingleCheckedArrayAdapter
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class BasicRecurrenceDialog : DialogFragment() {
|
||||||
|
@Inject lateinit var context: Activity
|
||||||
|
@Inject lateinit var dialogBuilder: DialogBuilder
|
||||||
|
@Inject lateinit var repeatRuleToString: RepeatRuleToString
|
||||||
|
|
||||||
|
private val customRecurrence =
|
||||||
|
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
targetFragment?.onActivityResult(targetRequestCode, it.resultCode, it.data)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val arguments = arguments
|
||||||
|
val dueDate = arguments!!.getLong(EXTRA_DATE, currentTimeMillis())
|
||||||
|
val rule = arguments.getString(EXTRA_RRULE)
|
||||||
|
var rrule: Recur? = null
|
||||||
|
try {
|
||||||
|
if (!isNullOrEmpty(rule)) {
|
||||||
|
rrule = newRecur(rule!!)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
}
|
||||||
|
val customPicked = isCustomValue(rrule)
|
||||||
|
val repeatOptions: List<String> =
|
||||||
|
Lists.newArrayList(*requireContext().resources.getStringArray(R.array.repeat_options))
|
||||||
|
val adapter = SingleCheckedArrayAdapter(requireContext(), repeatOptions)
|
||||||
|
var selected = 0
|
||||||
|
if (customPicked) {
|
||||||
|
adapter.insert(repeatRuleToString!!.toString(rule), 0)
|
||||||
|
} else if (rrule != null) {
|
||||||
|
selected = when (rrule.frequency) {
|
||||||
|
Recur.Frequency.DAILY -> 1
|
||||||
|
Recur.Frequency.WEEKLY -> 2
|
||||||
|
Recur.Frequency.MONTHLY -> 3
|
||||||
|
Recur.Frequency.YEARLY -> 4
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dialogBuilder
|
||||||
|
.newDialog()
|
||||||
|
.setSingleChoiceItems(
|
||||||
|
adapter,
|
||||||
|
selected
|
||||||
|
) { dialogInterface: DialogInterface, i: Int ->
|
||||||
|
var i = i
|
||||||
|
if (customPicked) {
|
||||||
|
if (i == 0) {
|
||||||
|
dialogInterface.dismiss()
|
||||||
|
return@setSingleChoiceItems
|
||||||
|
}
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
val result: Recur?
|
||||||
|
when (i) {
|
||||||
|
0 -> result = null
|
||||||
|
5 -> {
|
||||||
|
customRecurrence.launch(newIntent(requireContext(), rule, dueDate))
|
||||||
|
return@setSingleChoiceItems
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
result = newRecur()
|
||||||
|
result.interval = 1
|
||||||
|
when (i) {
|
||||||
|
1 -> result.setFrequency(Recur.Frequency.DAILY.name)
|
||||||
|
2 -> result.setFrequency(Recur.Frequency.WEEKLY.name)
|
||||||
|
3 -> result.setFrequency(Recur.Frequency.MONTHLY.name)
|
||||||
|
4 -> result.setFrequency(Recur.Frequency.YEARLY.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val intent = Intent()
|
||||||
|
intent.putExtra(EXTRA_RRULE, result?.toString())
|
||||||
|
targetFragment!!.onActivityResult(targetRequestCode, RESULT_OK, intent)
|
||||||
|
dialogInterface.dismiss()
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isCustomValue(rrule: Recur?): Boolean {
|
||||||
|
if (rrule == null) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
val frequency = rrule.frequency
|
||||||
|
return (frequency == Recur.Frequency.WEEKLY || frequency == Recur.Frequency.MONTHLY) && !rrule.dayList.isEmpty() || frequency == Recur.Frequency.HOURLY || frequency == Recur.Frequency.MINUTELY || rrule.until != null || rrule.interval > 1 || rrule.count > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val EXTRA_RRULE = "extra_rrule"
|
||||||
|
private const val EXTRA_DATE = "extra_date"
|
||||||
|
fun newBasicRecurrenceDialog(
|
||||||
|
target: Fragment?, rc: Int, rrule: String?, dueDate: Long
|
||||||
|
): BasicRecurrenceDialog {
|
||||||
|
val dialog = BasicRecurrenceDialog()
|
||||||
|
dialog.setTargetFragment(target, rc)
|
||||||
|
val arguments = Bundle()
|
||||||
|
if (rrule != null) {
|
||||||
|
arguments.putString(EXTRA_RRULE, rrule)
|
||||||
|
}
|
||||||
|
arguments.putLong(EXTRA_DATE, dueDate)
|
||||||
|
dialog.arguments = arguments
|
||||||
|
return dialog
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,53 @@
|
|||||||
|
package org.tasks.repeats
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.viewModels
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.fragment.app.FragmentActivity
|
||||||
|
import com.google.android.material.composethemeadapter.MdcTheme
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import org.tasks.compose.collectAsStateLifecycleAware
|
||||||
|
import org.tasks.compose.pickers.CustomRecurrence
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class CustomRecurrenceActivity : FragmentActivity() {
|
||||||
|
val viewModel: CustomRecurrenceViewModel by viewModels()
|
||||||
|
|
||||||
|
override fun onPostCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onPostCreate(savedInstanceState)
|
||||||
|
setContent {
|
||||||
|
MdcTheme {
|
||||||
|
CustomRecurrence(
|
||||||
|
state = viewModel.state.collectAsStateLifecycleAware().value,
|
||||||
|
save = {
|
||||||
|
setResult(RESULT_OK, Intent().putExtra(EXTRA_RRULE, viewModel.getRecur()))
|
||||||
|
finish()
|
||||||
|
},
|
||||||
|
discard = { finish() },
|
||||||
|
setSelectedFrequency = { viewModel.setFrequency(it) },
|
||||||
|
setEndDate = { viewModel.setEndDate(it) },
|
||||||
|
setSelectedEndType = { viewModel.setEndType(it) },
|
||||||
|
setInterval = { viewModel.setInterval(it) },
|
||||||
|
setOccurrences = { viewModel.setOccurrences(it) },
|
||||||
|
toggleDay = { viewModel.toggleDay(it) },
|
||||||
|
setMonthSelection = { viewModel.setMonthSelection(it) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val EXTRA_RRULE = "extra_rrule"
|
||||||
|
const val EXTRA_DATE = "extra_date"
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun newIntent(context: Context, rrule: String?, dueDate: Long) =
|
||||||
|
Intent(context, CustomRecurrenceActivity::class.java)
|
||||||
|
.putExtra(EXTRA_DATE, dueDate)
|
||||||
|
.putExtra(EXTRA_RRULE, rrule)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,557 +0,0 @@
|
|||||||
package org.tasks.repeats;
|
|
||||||
|
|
||||||
import static android.app.Activity.RESULT_OK;
|
|
||||||
import static com.google.common.collect.Lists.newArrayList;
|
|
||||||
import static net.fortuna.ical4j.model.Recur.Frequency.DAILY;
|
|
||||||
import static net.fortuna.ical4j.model.Recur.Frequency.HOURLY;
|
|
||||||
import static net.fortuna.ical4j.model.Recur.Frequency.MINUTELY;
|
|
||||||
import static net.fortuna.ical4j.model.Recur.Frequency.MONTHLY;
|
|
||||||
import static net.fortuna.ical4j.model.Recur.Frequency.WEEKLY;
|
|
||||||
import static net.fortuna.ical4j.model.Recur.Frequency.YEARLY;
|
|
||||||
import static org.tasks.Strings.isNullOrEmpty;
|
|
||||||
import static org.tasks.dialogs.MyDatePickerDialog.newDatePicker;
|
|
||||||
import static org.tasks.repeats.BasicRecurrenceDialog.EXTRA_RRULE;
|
|
||||||
import static org.tasks.repeats.RecurrenceUtils.newRecur;
|
|
||||||
import static org.tasks.time.DateTimeUtils.currentTimeMillis;
|
|
||||||
import static java.util.Arrays.asList;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.Dialog;
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.res.ColorStateList;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.graphics.drawable.ColorDrawable;
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.graphics.drawable.GradientDrawable;
|
|
||||||
import android.graphics.drawable.LayerDrawable;
|
|
||||||
import android.graphics.drawable.StateListDrawable;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.AdapterView;
|
|
||||||
import android.widget.ArrayAdapter;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.RadioButton;
|
|
||||||
import android.widget.RadioGroup;
|
|
||||||
import android.widget.Spinner;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.ToggleButton;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.widget.AppCompatSpinner;
|
|
||||||
import androidx.fragment.app.DialogFragment;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
|
|
||||||
import com.todoroo.andlib.utility.DateUtilities;
|
|
||||||
|
|
||||||
import net.fortuna.ical4j.model.Recur;
|
|
||||||
import net.fortuna.ical4j.model.Recur.Frequency;
|
|
||||||
import net.fortuna.ical4j.model.WeekDay;
|
|
||||||
|
|
||||||
import org.tasks.R;
|
|
||||||
import org.tasks.databinding.ControlSetRepeatBinding;
|
|
||||||
import org.tasks.databinding.WeekButtonsBinding;
|
|
||||||
import org.tasks.dialogs.DialogBuilder;
|
|
||||||
import org.tasks.dialogs.MyDatePickerDialog;
|
|
||||||
import org.tasks.extensions.LocaleKt;
|
|
||||||
import org.tasks.preferences.ResourceResolver;
|
|
||||||
import org.tasks.time.DateTime;
|
|
||||||
import org.tasks.ui.OnItemSelected;
|
|
||||||
import org.tasks.ui.OnTextChanged;
|
|
||||||
|
|
||||||
import java.text.DateFormatSymbols;
|
|
||||||
import java.time.format.FormatStyle;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
@AndroidEntryPoint
|
|
||||||
public class CustomRecurrenceDialog extends DialogFragment {
|
|
||||||
|
|
||||||
private static final List<Frequency> FREQUENCIES =
|
|
||||||
asList(MINUTELY, HOURLY, DAILY, WEEKLY, MONTHLY, YEARLY);
|
|
||||||
private static final String EXTRA_DATE = "extra_date";
|
|
||||||
private static final String FRAG_TAG_DATE_PICKER = "frag_tag_date_picker";
|
|
||||||
private static final int REQUEST_PICK_DATE = 505;
|
|
||||||
private final List<String> repeatUntilOptions = new ArrayList<>();
|
|
||||||
@Inject Activity context;
|
|
||||||
@Inject DialogBuilder dialogBuilder;
|
|
||||||
@Inject Locale locale;
|
|
||||||
|
|
||||||
private LinearLayout weekGroup1;
|
|
||||||
@Nullable private LinearLayout weekGroup2;
|
|
||||||
private RadioGroup monthGroup;
|
|
||||||
private RadioButton repeatMonthlyDayOfNthWeek;
|
|
||||||
private RadioButton repeatMonthlyDayOfLastWeek;
|
|
||||||
private Spinner repeatUntilSpinner;
|
|
||||||
private EditText intervalEditText;
|
|
||||||
private TextView intervalTextView;
|
|
||||||
private EditText repeatTimes;
|
|
||||||
private TextView repeatTimesText;
|
|
||||||
|
|
||||||
private ArrayAdapter<String> repeatUntilAdapter;
|
|
||||||
private ToggleButton[] weekButtons;
|
|
||||||
private Recur rrule;
|
|
||||||
private long dueDate;
|
|
||||||
|
|
||||||
public static CustomRecurrenceDialog newCustomRecurrenceDialog(
|
|
||||||
Fragment target, int rc, String rrule, long dueDate) {
|
|
||||||
CustomRecurrenceDialog dialog = new CustomRecurrenceDialog();
|
|
||||||
dialog.setTargetFragment(target, rc);
|
|
||||||
Bundle arguments = new Bundle();
|
|
||||||
if (rrule != null) {
|
|
||||||
arguments.putString(EXTRA_RRULE, rrule);
|
|
||||||
}
|
|
||||||
arguments.putLong(EXTRA_DATE, dueDate);
|
|
||||||
dialog.setArguments(arguments);
|
|
||||||
return dialog;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
|
||||||
LayoutInflater inflater = LayoutInflater.from(context);
|
|
||||||
ControlSetRepeatBinding binding = ControlSetRepeatBinding.inflate(inflater);
|
|
||||||
WeekButtonsBinding weekBinding = WeekButtonsBinding.bind(binding.getRoot());
|
|
||||||
|
|
||||||
Bundle arguments = getArguments();
|
|
||||||
dueDate = arguments.getLong(EXTRA_DATE, currentTimeMillis());
|
|
||||||
String rule =
|
|
||||||
savedInstanceState == null
|
|
||||||
? arguments.getString(EXTRA_RRULE)
|
|
||||||
: savedInstanceState.getString(EXTRA_RRULE);
|
|
||||||
try {
|
|
||||||
if (!isNullOrEmpty(rule)) {
|
|
||||||
rrule = newRecur(rule);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
Timber.e(e);
|
|
||||||
}
|
|
||||||
if (rrule == null) {
|
|
||||||
rrule = newRecur();
|
|
||||||
rrule.setInterval(1);
|
|
||||||
rrule.setFrequency(WEEKLY.name());
|
|
||||||
}
|
|
||||||
|
|
||||||
DateFormatSymbols dfs = new DateFormatSymbols(locale);
|
|
||||||
String[] shortWeekdays = dfs.getShortWeekdays();
|
|
||||||
|
|
||||||
weekGroup1 = weekBinding.weekGroup;
|
|
||||||
weekGroup2 = weekBinding.weekGroup2;
|
|
||||||
monthGroup = binding.monthGroup;
|
|
||||||
repeatMonthlyDayOfNthWeek = binding.repeatMonthlyDayOfNthWeek;
|
|
||||||
repeatMonthlyDayOfLastWeek = binding.repeatMonthlyDayOfLastWeek;
|
|
||||||
repeatUntilSpinner = binding.repeatUntil;
|
|
||||||
repeatUntilSpinner.setOnItemSelectedListener(new OnItemSelected() {
|
|
||||||
@Override
|
|
||||||
public void onItemSelected(int position) {
|
|
||||||
onRepeatUntilChanged(position);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
intervalEditText = binding.intervalValue;
|
|
||||||
intervalEditText.addTextChangedListener(new OnTextChanged() {
|
|
||||||
@Override
|
|
||||||
public void onTextChanged(@Nullable CharSequence text) {
|
|
||||||
if (text != null) {
|
|
||||||
onRepeatValueChanged(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
intervalTextView = binding.intervalText;
|
|
||||||
repeatTimes = binding.repeatTimesValue;
|
|
||||||
repeatTimes.addTextChangedListener(new OnTextChanged() {
|
|
||||||
@Override
|
|
||||||
public void onTextChanged(@Nullable CharSequence text) {
|
|
||||||
if (text != null) {
|
|
||||||
onRepeatTimesValueChanged(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
repeatTimesText = binding.repeatTimesText;
|
|
||||||
AppCompatSpinner frequency = binding.frequency;
|
|
||||||
frequency.setOnItemSelectedListener(new OnItemSelected() {
|
|
||||||
@Override
|
|
||||||
public void onItemSelected(int position) {
|
|
||||||
onFrequencyChanged(position);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Calendar dayOfMonthCalendar = Calendar.getInstance(locale);
|
|
||||||
dayOfMonthCalendar.setTimeInMillis(dueDate);
|
|
||||||
int dayOfWeekInMonth = dayOfMonthCalendar.get(Calendar.DAY_OF_WEEK_IN_MONTH);
|
|
||||||
int maxDayOfWeekInMonth = dayOfMonthCalendar.getActualMaximum(Calendar.DAY_OF_WEEK_IN_MONTH);
|
|
||||||
|
|
||||||
int dueDayOfWeek = dayOfMonthCalendar.get(Calendar.DAY_OF_WEEK);
|
|
||||||
String today = dfs.getWeekdays()[dueDayOfWeek];
|
|
||||||
if (dayOfWeekInMonth == maxDayOfWeekInMonth) {
|
|
||||||
repeatMonthlyDayOfLastWeek.setVisibility(View.VISIBLE);
|
|
||||||
String last = getString(R.string.repeat_monthly_last_week);
|
|
||||||
String text = getString(R.string.repeat_monthly_on_every_day_of_nth_week, last, today);
|
|
||||||
repeatMonthlyDayOfLastWeek.setTag(new WeekDay(calendarDayToWeekday(dueDayOfWeek), -1));
|
|
||||||
repeatMonthlyDayOfLastWeek.setText(text);
|
|
||||||
} else {
|
|
||||||
repeatMonthlyDayOfLastWeek.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dayOfWeekInMonth < 6) {
|
|
||||||
int[] resources =
|
|
||||||
new int[] {
|
|
||||||
R.string.repeat_monthly_first_week,
|
|
||||||
R.string.repeat_monthly_second_week,
|
|
||||||
R.string.repeat_monthly_third_week,
|
|
||||||
R.string.repeat_monthly_fourth_week,
|
|
||||||
R.string.repeat_monthly_fifth_week,
|
|
||||||
};
|
|
||||||
repeatMonthlyDayOfNthWeek.setVisibility(View.VISIBLE);
|
|
||||||
String nth = getString(resources[dayOfWeekInMonth - 1]);
|
|
||||||
String text = getString(R.string.repeat_monthly_on_every_day_of_nth_week, nth, today);
|
|
||||||
repeatMonthlyDayOfNthWeek.setTag(
|
|
||||||
new WeekDay(calendarDayToWeekday(dueDayOfWeek), dayOfWeekInMonth));
|
|
||||||
repeatMonthlyDayOfNthWeek.setText(text);
|
|
||||||
} else {
|
|
||||||
repeatMonthlyDayOfNthWeek.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rrule.getFrequency() == MONTHLY) {
|
|
||||||
if (rrule.getDayList().size() == 1) {
|
|
||||||
WeekDay weekday = rrule.getDayList().get(0);
|
|
||||||
if (weekday.getOffset() == -1) {
|
|
||||||
repeatMonthlyDayOfLastWeek.setChecked(true);
|
|
||||||
} else if (weekday.getOffset() == dayOfWeekInMonth) {
|
|
||||||
repeatMonthlyDayOfNthWeek.setChecked(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (monthGroup.getCheckedRadioButtonId() != R.id.repeat_monthly_day_of_last_week
|
|
||||||
&& monthGroup.getCheckedRadioButtonId() != R.id.repeat_monthly_day_of_nth_week) {
|
|
||||||
binding.repeatMonthlySameDay.setChecked(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
ArrayAdapter<CharSequence> frequencyAdapter =
|
|
||||||
ArrayAdapter.createFromResource(context, R.array.repeat_frequency, R.layout.frequency_item);
|
|
||||||
frequencyAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
|
||||||
frequency.setAdapter(frequencyAdapter);
|
|
||||||
frequency.setSelection(FREQUENCIES.indexOf(rrule.getFrequency()));
|
|
||||||
|
|
||||||
intervalEditText.setText(LocaleKt.formatNumber(locale, rrule.getInterval()));
|
|
||||||
|
|
||||||
repeatUntilAdapter =
|
|
||||||
new ArrayAdapter<String>(context, 0, repeatUntilOptions) {
|
|
||||||
@Override
|
|
||||||
public View getDropDownView(
|
|
||||||
int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
|
||||||
ViewGroup vg =
|
|
||||||
(ViewGroup) inflater.inflate(R.layout.simple_spinner_dropdown_item, parent, false);
|
|
||||||
((TextView) vg.findViewById(R.id.text1)).setText(getItem(position));
|
|
||||||
return vg;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
|
|
||||||
int selectedItemPosition = position;
|
|
||||||
if (parent instanceof AdapterView) {
|
|
||||||
selectedItemPosition = ((AdapterView) parent).getSelectedItemPosition();
|
|
||||||
}
|
|
||||||
TextView tv =
|
|
||||||
(TextView) inflater.inflate(android.R.layout.simple_spinner_item, parent, false);
|
|
||||||
tv.setPadding(0, 0, 0, 0);
|
|
||||||
tv.setText(repeatUntilOptions.get(selectedItemPosition));
|
|
||||||
return tv;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
repeatUntilAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
|
||||||
repeatUntilSpinner.setAdapter(repeatUntilAdapter);
|
|
||||||
updateRepeatUntilOptions();
|
|
||||||
|
|
||||||
weekButtons = new ToggleButton[]{
|
|
||||||
weekBinding.weekDay1.button,
|
|
||||||
weekBinding.weekDay2.button,
|
|
||||||
weekBinding.weekDay3.button,
|
|
||||||
weekBinding.weekDay4.button,
|
|
||||||
weekBinding.weekDay5.button,
|
|
||||||
weekBinding.weekDay6.button,
|
|
||||||
weekBinding.weekDay7.button
|
|
||||||
};
|
|
||||||
|
|
||||||
// set up days of week
|
|
||||||
Calendar dayOfWeekCalendar = Calendar.getInstance(locale);
|
|
||||||
dayOfWeekCalendar.set(Calendar.DAY_OF_WEEK, dayOfWeekCalendar.getFirstDayOfWeek());
|
|
||||||
|
|
||||||
WeekDay todayWeekday = new WeekDay(new DateTime(dueDate).getWeekDay(), 0);
|
|
||||||
|
|
||||||
ColorStateList colorStateList =
|
|
||||||
new ColorStateList(
|
|
||||||
new int[][] {
|
|
||||||
new int[] {android.R.attr.state_checked}, new int[] {-android.R.attr.state_checked}
|
|
||||||
},
|
|
||||||
new int[] {
|
|
||||||
ResourceResolver.getData(context, com.google.android.material.R.attr.colorOnSecondary),
|
|
||||||
context.getColor(R.color.text_primary)
|
|
||||||
});
|
|
||||||
int inset = (int) context.getResources().getDimension(R.dimen.week_button_inset);
|
|
||||||
int accentColor = ResourceResolver.getData(context, androidx.appcompat.R.attr.colorAccent);
|
|
||||||
int animationDuration =
|
|
||||||
context.getResources().getInteger(android.R.integer.config_shortAnimTime);
|
|
||||||
|
|
||||||
for (int i = 0; i < 7; i++) {
|
|
||||||
ToggleButton weekButton = weekButtons[i];
|
|
||||||
|
|
||||||
GradientDrawable ovalDrawable =
|
|
||||||
(GradientDrawable)
|
|
||||||
context.getDrawable(R.drawable.week_day_button_oval).mutate();
|
|
||||||
ovalDrawable.setColor(accentColor);
|
|
||||||
LayerDrawable layerDrawable = new LayerDrawable(new Drawable[] {ovalDrawable});
|
|
||||||
layerDrawable.setLayerInset(0, inset, inset, inset, inset);
|
|
||||||
StateListDrawable stateListDrawable = new StateListDrawable();
|
|
||||||
stateListDrawable.setEnterFadeDuration(animationDuration);
|
|
||||||
stateListDrawable.setExitFadeDuration(animationDuration);
|
|
||||||
stateListDrawable.addState(
|
|
||||||
new int[] {-android.R.attr.state_checked}, new ColorDrawable(Color.TRANSPARENT));
|
|
||||||
stateListDrawable.addState(new int[] {android.R.attr.state_checked}, layerDrawable);
|
|
||||||
int paddingBottom = weekButton.getPaddingBottom();
|
|
||||||
int paddingTop = weekButton.getPaddingTop();
|
|
||||||
int paddingLeft = weekButton.getPaddingLeft();
|
|
||||||
int paddingRight = weekButton.getPaddingRight();
|
|
||||||
weekButton.setBackground(stateListDrawable);
|
|
||||||
weekButton.setPadding(paddingLeft, paddingTop, paddingRight, paddingBottom);
|
|
||||||
|
|
||||||
int dayOfWeek = dayOfWeekCalendar.get(Calendar.DAY_OF_WEEK);
|
|
||||||
String text = shortWeekdays[dayOfWeek];
|
|
||||||
weekButton.setTextColor(colorStateList);
|
|
||||||
weekButton.setTextOn(text);
|
|
||||||
weekButton.setTextOff(text);
|
|
||||||
weekButton.setTag(new WeekDay(calendarDayToWeekday(dayOfWeek), 0));
|
|
||||||
if (savedInstanceState == null) {
|
|
||||||
weekButton.setChecked(
|
|
||||||
rrule.getFrequency() != WEEKLY || rrule.getDayList().isEmpty()
|
|
||||||
? todayWeekday.equals(weekButton.getTag())
|
|
||||||
: rrule.getDayList().contains(weekButton.getTag()));
|
|
||||||
}
|
|
||||||
dayOfWeekCalendar.add(Calendar.DATE, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
setCancelable(false);
|
|
||||||
|
|
||||||
return dialogBuilder
|
|
||||||
.newDialog()
|
|
||||||
.setView(binding.getRoot())
|
|
||||||
.setPositiveButton(R.string.ok, this::onRuleSelected)
|
|
||||||
.setNegativeButton(R.string.cancel, null)
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onRuleSelected(DialogInterface dialogInterface, int which) {
|
|
||||||
if (rrule.getFrequency() == WEEKLY) {
|
|
||||||
List<WeekDay> checked = new ArrayList<>();
|
|
||||||
for (ToggleButton weekButton : weekButtons) {
|
|
||||||
if (weekButton.isChecked()) {
|
|
||||||
checked.add((WeekDay) weekButton.getTag());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rrule.getDayList().clear();
|
|
||||||
rrule.getDayList().addAll(checked);
|
|
||||||
} else if (rrule.getFrequency() == MONTHLY) {
|
|
||||||
int checkedRadioButtonId = monthGroup.getCheckedRadioButtonId();
|
|
||||||
if (checkedRadioButtonId == R.id.repeat_monthly_same_day) {
|
|
||||||
rrule.getDayList().clear();
|
|
||||||
} else if (checkedRadioButtonId == R.id.repeat_monthly_day_of_nth_week) {
|
|
||||||
rrule.getDayList().clear();
|
|
||||||
rrule.getDayList().addAll(newArrayList((WeekDay) repeatMonthlyDayOfNthWeek.getTag()));
|
|
||||||
} else if (checkedRadioButtonId == R.id.repeat_monthly_day_of_last_week) {
|
|
||||||
rrule.getDayList().clear();
|
|
||||||
rrule.getDayList().addAll(newArrayList((WeekDay) repeatMonthlyDayOfLastWeek.getTag()));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
rrule.getDayList().clear();
|
|
||||||
}
|
|
||||||
Intent intent = new Intent();
|
|
||||||
intent.putExtra(EXTRA_RRULE, rrule.toString());
|
|
||||||
getTargetFragment().onActivityResult(getTargetRequestCode(), RESULT_OK, intent);
|
|
||||||
dismiss();
|
|
||||||
}
|
|
||||||
|
|
||||||
private WeekDay calendarDayToWeekday(int calendarDay) {
|
|
||||||
switch (calendarDay) {
|
|
||||||
case Calendar.SUNDAY:
|
|
||||||
return WeekDay.SU;
|
|
||||||
case Calendar.MONDAY:
|
|
||||||
return WeekDay.MO;
|
|
||||||
case Calendar.TUESDAY:
|
|
||||||
return WeekDay.TU;
|
|
||||||
case Calendar.WEDNESDAY:
|
|
||||||
return WeekDay.WE;
|
|
||||||
case Calendar.THURSDAY:
|
|
||||||
return WeekDay.TH;
|
|
||||||
case Calendar.FRIDAY:
|
|
||||||
return WeekDay.FR;
|
|
||||||
case Calendar.SATURDAY:
|
|
||||||
return WeekDay.SA;
|
|
||||||
}
|
|
||||||
throw new RuntimeException("Invalid calendar day: " + calendarDay);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSaveInstanceState(Bundle outState) {
|
|
||||||
super.onSaveInstanceState(outState);
|
|
||||||
|
|
||||||
outState.putString(EXTRA_RRULE, rrule.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setInterval(int interval, boolean updateEditText) {
|
|
||||||
rrule.setInterval(interval);
|
|
||||||
if (updateEditText) {
|
|
||||||
intervalEditText.setText(LocaleKt.formatNumber(locale, interval));
|
|
||||||
}
|
|
||||||
updateIntervalTextView();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateIntervalTextView() {
|
|
||||||
int resource = getFrequencyPlural();
|
|
||||||
String quantityString = getResources().getQuantityString(resource, rrule.getInterval());
|
|
||||||
intervalTextView.setText(quantityString);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setCount(int count, boolean updateEditText) {
|
|
||||||
rrule.setCount(count);
|
|
||||||
if (updateEditText) {
|
|
||||||
intervalEditText.setText(LocaleKt.formatNumber(locale, count));
|
|
||||||
}
|
|
||||||
updateCountText();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateCountText() {
|
|
||||||
repeatTimesText.setText(
|
|
||||||
getResources().getQuantityString(R.plurals.repeat_times, rrule.getCount()));
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getFrequencyPlural() {
|
|
||||||
switch (rrule.getFrequency()) {
|
|
||||||
case MINUTELY:
|
|
||||||
return R.plurals.repeat_minutes;
|
|
||||||
case HOURLY:
|
|
||||||
return R.plurals.repeat_hours;
|
|
||||||
case DAILY:
|
|
||||||
return R.plurals.repeat_days;
|
|
||||||
case WEEKLY:
|
|
||||||
return R.plurals.repeat_weeks;
|
|
||||||
case MONTHLY:
|
|
||||||
return R.plurals.repeat_months;
|
|
||||||
case YEARLY:
|
|
||||||
return R.plurals.repeat_years;
|
|
||||||
default:
|
|
||||||
throw new RuntimeException("Invalid frequency: " + rrule.getFrequency());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onRepeatUntilChanged(int position) {
|
|
||||||
if (repeatUntilOptions.size() == 4) {
|
|
||||||
position--;
|
|
||||||
}
|
|
||||||
if (position == 0) {
|
|
||||||
rrule.setUntil(null);
|
|
||||||
updateRepeatUntilOptions();
|
|
||||||
} else if (position == 1) {
|
|
||||||
repeatUntilClick();
|
|
||||||
} else if (position == 2) {
|
|
||||||
rrule.setUntil(null);
|
|
||||||
rrule.setCount(Math.max(rrule.getCount(), 1));
|
|
||||||
updateRepeatUntilOptions();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onFrequencyChanged(int position) {
|
|
||||||
Frequency frequency = FREQUENCIES.get(position);
|
|
||||||
rrule.setFrequency(frequency.name());
|
|
||||||
int weekVisibility = frequency == WEEKLY ? View.VISIBLE : View.GONE;
|
|
||||||
weekGroup1.setVisibility(weekVisibility);
|
|
||||||
if (weekGroup2 != null) {
|
|
||||||
weekGroup2.setVisibility(weekVisibility);
|
|
||||||
}
|
|
||||||
monthGroup.setVisibility(frequency == MONTHLY && dueDate >= 0 ? View.VISIBLE : View.GONE);
|
|
||||||
updateIntervalTextView();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onRepeatValueChanged(CharSequence text) {
|
|
||||||
Integer value = LocaleKt.parseInteger(locale, text.toString());
|
|
||||||
if (value == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (value < 1) {
|
|
||||||
setInterval(1, true);
|
|
||||||
} else {
|
|
||||||
setInterval(value, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onRepeatTimesValueChanged(CharSequence text) {
|
|
||||||
Integer value = LocaleKt.parseInteger(locale, text.toString());
|
|
||||||
if (value == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (value < 1) {
|
|
||||||
setCount(1, true);
|
|
||||||
} else {
|
|
||||||
setCount(value, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void repeatUntilClick() {
|
|
||||||
if (getParentFragmentManager().findFragmentByTag(FRAG_TAG_DATE_PICKER) == null) {
|
|
||||||
long repeatUntil = DateTime.from(rrule.getUntil()).getMillis();
|
|
||||||
newDatePicker(this, REQUEST_PICK_DATE, repeatUntil > 0 ? repeatUntil : 0L)
|
|
||||||
.show(getParentFragmentManager(), FRAG_TAG_DATE_PICKER);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateRepeatUntilOptions() {
|
|
||||||
repeatUntilOptions.clear();
|
|
||||||
long repeatUntil = DateTime.from(rrule.getUntil()).getMillis();
|
|
||||||
int count = rrule.getCount();
|
|
||||||
if (repeatUntil > 0) {
|
|
||||||
repeatUntilOptions.add(
|
|
||||||
getString(
|
|
||||||
R.string.repeat_until,
|
|
||||||
DateUtilities.getRelativeDateTime(
|
|
||||||
context, repeatUntil, locale, FormatStyle.MEDIUM, true)));
|
|
||||||
repeatTimes.setVisibility(View.GONE);
|
|
||||||
repeatTimesText.setVisibility(View.GONE);
|
|
||||||
} else if (count > 0) {
|
|
||||||
repeatUntilOptions.add(getString(R.string.repeat_occurs));
|
|
||||||
repeatTimes.setText(LocaleKt.formatNumber(locale, count));
|
|
||||||
repeatTimes.setVisibility(View.VISIBLE);
|
|
||||||
updateCountText();
|
|
||||||
repeatTimesText.setVisibility(View.VISIBLE);
|
|
||||||
} else {
|
|
||||||
repeatTimes.setVisibility(View.GONE);
|
|
||||||
repeatTimesText.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
repeatUntilOptions.add(getString(R.string.repeat_forever));
|
|
||||||
repeatUntilOptions.add(getString(R.string.repeat_until, "").trim());
|
|
||||||
repeatUntilOptions.add(getString(R.string.repeat_number_of_times));
|
|
||||||
repeatUntilAdapter.notifyDataSetChanged();
|
|
||||||
repeatUntilSpinner.setSelection(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
|
||||||
if (requestCode == REQUEST_PICK_DATE) {
|
|
||||||
if (resultCode == RESULT_OK) {
|
|
||||||
rrule.setUntil(
|
|
||||||
new DateTime(data.getLongExtra(MyDatePickerDialog.EXTRA_TIMESTAMP, 0L)).toDate());
|
|
||||||
}
|
|
||||||
updateRepeatUntilOptions();
|
|
||||||
}
|
|
||||||
super.onActivityResult(requestCode, resultCode, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,224 @@
|
|||||||
|
package org.tasks.repeats
|
||||||
|
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import net.fortuna.ical4j.model.Date
|
||||||
|
import net.fortuna.ical4j.model.Recur
|
||||||
|
import net.fortuna.ical4j.model.Recur.Frequency.DAILY
|
||||||
|
import net.fortuna.ical4j.model.Recur.Frequency.HOURLY
|
||||||
|
import net.fortuna.ical4j.model.Recur.Frequency.MINUTELY
|
||||||
|
import net.fortuna.ical4j.model.Recur.Frequency.MONTHLY
|
||||||
|
import net.fortuna.ical4j.model.Recur.Frequency.WEEKLY
|
||||||
|
import net.fortuna.ical4j.model.Recur.Frequency.YEARLY
|
||||||
|
import net.fortuna.ical4j.model.WeekDay
|
||||||
|
import net.fortuna.ical4j.model.WeekDayList
|
||||||
|
import net.fortuna.ical4j.model.property.RRule
|
||||||
|
import org.tasks.date.DateTimeUtils.toDateTime
|
||||||
|
import org.tasks.repeats.CustomRecurrenceActivity.Companion.EXTRA_DATE
|
||||||
|
import org.tasks.repeats.CustomRecurrenceActivity.Companion.EXTRA_RRULE
|
||||||
|
import org.tasks.time.DateTime
|
||||||
|
import org.tasks.time.DateTimeUtils.startOfDay
|
||||||
|
import java.time.DayOfWeek
|
||||||
|
import java.time.Instant
|
||||||
|
import java.time.ZoneId
|
||||||
|
import java.time.temporal.WeekFields
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Calendar.DAY_OF_WEEK_IN_MONTH
|
||||||
|
import java.util.Locale
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class CustomRecurrenceViewModel @Inject constructor(
|
||||||
|
savedStateHandle: SavedStateHandle,
|
||||||
|
locale: Locale,
|
||||||
|
) : ViewModel() {
|
||||||
|
data class ViewState(
|
||||||
|
val interval: Int = 1,
|
||||||
|
val frequency: Recur.Frequency = WEEKLY,
|
||||||
|
val dueDate: Long = DateTime().startOfDay().millis,
|
||||||
|
val endSelection: Int = 0,
|
||||||
|
val endDate: Long = dueDate.toDateTime().plusMonths(1).startOfDay().millis,
|
||||||
|
val endCount: Int = 1,
|
||||||
|
val frequencyOptions: List<Recur.Frequency> = DEFAULT_FREQUENCIES,
|
||||||
|
val daysOfWeek: List<DayOfWeek> = Locale.getDefault().daysOfWeek(),
|
||||||
|
val selectedDays: List<DayOfWeek> = emptyList(),
|
||||||
|
val locale: Locale = Locale.getDefault(),
|
||||||
|
val monthDay: WeekDay? = null,
|
||||||
|
) {
|
||||||
|
val dueDayOfWeek: DayOfWeek
|
||||||
|
get() = Instant.ofEpochMilli(dueDate).atZone(ZoneId.systemDefault()).dayOfWeek
|
||||||
|
|
||||||
|
val dueDayOfMonth: Int
|
||||||
|
get() = DateTime(dueDate).dayOfMonth
|
||||||
|
|
||||||
|
val nthWeek: Int
|
||||||
|
get() =
|
||||||
|
Calendar.getInstance(locale)
|
||||||
|
.apply { timeInMillis = dueDate }
|
||||||
|
.get(DAY_OF_WEEK_IN_MONTH)
|
||||||
|
|
||||||
|
val lastWeekDayOfMonth: Boolean
|
||||||
|
get() =
|
||||||
|
Calendar.getInstance(locale)
|
||||||
|
.apply { timeInMillis = dueDate }
|
||||||
|
.let { it[DAY_OF_WEEK_IN_MONTH] == it.getActualMaximum(DAY_OF_WEEK_IN_MONTH) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _state = MutableStateFlow(ViewState())
|
||||||
|
val state = _state.asStateFlow()
|
||||||
|
|
||||||
|
init {
|
||||||
|
val daysOfWeek = locale.daysOfWeek()
|
||||||
|
val recur = savedStateHandle.get<String>(EXTRA_RRULE)?.let { RRule(it) }?.recur
|
||||||
|
val dueDate = savedStateHandle
|
||||||
|
.get<Long>(EXTRA_DATE)
|
||||||
|
?.takeIf { it > 0 }
|
||||||
|
?: System.currentTimeMillis().startOfDay()
|
||||||
|
val selectedDays = if (recur?.frequency == WEEKLY) {
|
||||||
|
recur.dayList?.toDaysOfWeek()
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
_state.update { state ->
|
||||||
|
state.copy(
|
||||||
|
interval = recur?.interval?.takeIf { it > 0 } ?: 1,
|
||||||
|
frequency = recur?.frequency ?: WEEKLY,
|
||||||
|
dueDate = dueDate,
|
||||||
|
endSelection = when {
|
||||||
|
recur == null -> 0
|
||||||
|
recur.until != null -> 1
|
||||||
|
recur.count >= 0 -> 2
|
||||||
|
else -> 0
|
||||||
|
},
|
||||||
|
endDate = DateTime(dueDate).plusMonths(1).startOfDay().millis,
|
||||||
|
endCount = recur?.count?.takeIf { it >= 0 } ?: 1,
|
||||||
|
daysOfWeek = daysOfWeek,
|
||||||
|
selectedDays = recur
|
||||||
|
?.dayList
|
||||||
|
?.takeIf { recur.frequency == WEEKLY }
|
||||||
|
?.toDaysOfWeek()
|
||||||
|
?: emptyList(),
|
||||||
|
locale = locale,
|
||||||
|
monthDay = recur?.dayList?.takeIf { recur.frequency == MONTHLY }?.firstOrNull(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setEndType(endType: Int) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(endSelection = endType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setFrequency(frequency: Recur.Frequency) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(frequency = frequency)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setEndDate(endDate: Long) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(endDate = endDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setInterval(interval: Int) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(interval = interval)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setOccurrences(occurrences: Int) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(endCount = occurrences)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleDay(dayOfWeek: DayOfWeek) {
|
||||||
|
_state.update { state ->
|
||||||
|
state.copy(
|
||||||
|
selectedDays = state.selectedDays.toMutableList().also {
|
||||||
|
if (!it.remove(dayOfWeek)) {
|
||||||
|
it.add(dayOfWeek)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getRecur(): String {
|
||||||
|
val state = _state.value
|
||||||
|
val builder = Recur.Builder().frequency(state.frequency)
|
||||||
|
if (state.frequency == WEEKLY) {
|
||||||
|
builder.dayList(state.selectedDays.toWeekDayList())
|
||||||
|
} else if (state.frequency == MONTHLY) {
|
||||||
|
state.monthDay?.let { builder.dayList(WeekDayList(it)) }
|
||||||
|
}
|
||||||
|
if (state.interval > 1) {
|
||||||
|
builder.interval(state.interval)
|
||||||
|
}
|
||||||
|
when (state.endSelection) {
|
||||||
|
1 -> builder.until(Date(state.endDate))
|
||||||
|
2 -> builder.count(state.endCount.coerceAtLeast(1))
|
||||||
|
}
|
||||||
|
return builder.build().toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setMonthSelection(selection: Int) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
monthDay = when (selection) {
|
||||||
|
0 -> null
|
||||||
|
1 -> WeekDay(it.dueDayOfWeek.weekDay, it.nthWeek)
|
||||||
|
2 -> WeekDay(it.dueDayOfWeek.weekDay, -1)
|
||||||
|
else -> throw IllegalArgumentException()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val DEFAULT_FREQUENCIES = listOf(MINUTELY, HOURLY, DAILY, WEEKLY, MONTHLY, YEARLY)
|
||||||
|
|
||||||
|
private fun Locale.daysOfWeek(): List<DayOfWeek> {
|
||||||
|
val values = DayOfWeek.values()
|
||||||
|
val weekFields = WeekFields.of(this)
|
||||||
|
var index = values.indexOf(weekFields.firstDayOfWeek)
|
||||||
|
return (0..6).map {
|
||||||
|
values[index].also { index = (index + 1) % 7 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun WeekDayList.toDaysOfWeek(): List<DayOfWeek> = map {
|
||||||
|
when (it) {
|
||||||
|
WeekDay.SU -> DayOfWeek.SUNDAY
|
||||||
|
WeekDay.MO -> DayOfWeek.MONDAY
|
||||||
|
WeekDay.TU -> DayOfWeek.TUESDAY
|
||||||
|
WeekDay.WE -> DayOfWeek.WEDNESDAY
|
||||||
|
WeekDay.TH -> DayOfWeek.THURSDAY
|
||||||
|
WeekDay.FR -> DayOfWeek.FRIDAY
|
||||||
|
WeekDay.SA -> DayOfWeek.SATURDAY
|
||||||
|
else -> throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<DayOfWeek>.toWeekDayList(): WeekDayList =
|
||||||
|
WeekDayList(*sortedBy { it.value }.map { it.weekDay }.toTypedArray())
|
||||||
|
|
||||||
|
private val DayOfWeek.weekDay: WeekDay
|
||||||
|
get() = when (this) {
|
||||||
|
DayOfWeek.SUNDAY -> WeekDay.SU
|
||||||
|
DayOfWeek.MONDAY -> WeekDay.MO
|
||||||
|
DayOfWeek.TUESDAY -> WeekDay.TU
|
||||||
|
DayOfWeek.WEDNESDAY -> WeekDay.WE
|
||||||
|
DayOfWeek.THURSDAY -> WeekDay.TH
|
||||||
|
DayOfWeek.FRIDAY -> WeekDay.FR
|
||||||
|
DayOfWeek.SATURDAY -> WeekDay.SA
|
||||||
|
else -> throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:shape="oval">
|
|
||||||
<corners android:radius="@dimen/week_button_state_on_circle_size"/>
|
|
||||||
</shape>
|
|
||||||
@ -1,57 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2015 Vikram Kakkar
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
-->
|
|
||||||
<merge xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/weekGroup"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:layout_marginStart="20dp"
|
|
||||||
android:layout_marginEnd="20dp"
|
|
||||||
android:gravity="center_horizontal"
|
|
||||||
android:orientation="horizontal">
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/week_day_1"
|
|
||||||
layout="@layout/week_day_button"/>
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/week_day_2"
|
|
||||||
layout="@layout/week_day_button"/>
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/week_day_3"
|
|
||||||
layout="@layout/week_day_button"/>
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/week_day_4"
|
|
||||||
layout="@layout/week_day_button"/>
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/week_day_5"
|
|
||||||
layout="@layout/week_day_button"/>
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/week_day_6"
|
|
||||||
layout="@layout/week_day_button"/>
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/week_day_7"
|
|
||||||
layout="@layout/week_day_button"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</merge>
|
|
||||||
@ -1,152 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!--
|
|
||||||
** Copyright (c) 2012 Todoroo Inc
|
|
||||||
**
|
|
||||||
** See the file "LICENSE" for the full license governing this code.
|
|
||||||
-->
|
|
||||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="fill_parent"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="?attr/colorAccent">
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatSpinner
|
|
||||||
android:id="@+id/frequency"
|
|
||||||
style="Widget.MaterialComponents.Spinner"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="4dp"
|
|
||||||
android:layout_marginBottom="4dp"
|
|
||||||
android:layout_gravity="center_horizontal"
|
|
||||||
android:paddingStart="@dimen/keyline_first"
|
|
||||||
android:paddingEnd="@dimen/keyline_first"
|
|
||||||
app:backgroundTint="?attr/colorOnSecondary"/>
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:focusableInTouchMode="true"
|
|
||||||
android:orientation="horizontal">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingStart="@dimen/keyline_first"
|
|
||||||
android:paddingEnd="0dp"
|
|
||||||
android:text="@string/repeat_every"
|
|
||||||
android:textAppearance="@style/TextAppearance"/>
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/intervalValue"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingStart="4dp"
|
|
||||||
android:paddingEnd="4dp"
|
|
||||||
android:ems="3"
|
|
||||||
android:focusable="true"
|
|
||||||
android:focusableInTouchMode="true"
|
|
||||||
android:gravity="center_horizontal"
|
|
||||||
android:imeOptions="flagNoExtractUi"
|
|
||||||
android:inputType="number"
|
|
||||||
android:maxLength="3"
|
|
||||||
android:selectAllOnFocus="true"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:textSize="15sp"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/intervalText"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingStart="0dp"
|
|
||||||
android:paddingEnd="@dimen/keyline_first"
|
|
||||||
android:textAppearance="@style/TextAppearance"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<include layout="@layout/week_buttons"/>
|
|
||||||
|
|
||||||
<RadioGroup
|
|
||||||
android:id="@+id/month_group"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingStart="@dimen/keyline_first"
|
|
||||||
android:paddingEnd="@dimen/keyline_first"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:visibility="gone">
|
|
||||||
|
|
||||||
<RadioButton
|
|
||||||
android:id="@+id/repeat_monthly_same_day"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/repeat_monthly_same_day_each_month"/>
|
|
||||||
|
|
||||||
<RadioButton
|
|
||||||
android:id="@+id/repeat_monthly_day_of_nth_week"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"/>
|
|
||||||
|
|
||||||
<RadioButton
|
|
||||||
android:id="@+id/repeat_monthly_day_of_last_week"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"/>
|
|
||||||
</RadioGroup>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:orientation="horizontal">
|
|
||||||
|
|
||||||
<Spinner
|
|
||||||
android:id="@+id/repeat_until"
|
|
||||||
style="Widget.MaterialComponents.Spinner"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="@dimen/keyline_first"
|
|
||||||
android:layout_marginEnd="0dp"
|
|
||||||
android:layout_gravity="center"/>
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/repeatTimesValue"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingStart="4dp"
|
|
||||||
android:paddingEnd="4dp"
|
|
||||||
android:ems="3"
|
|
||||||
android:focusable="true"
|
|
||||||
android:focusableInTouchMode="true"
|
|
||||||
android:gravity="center"
|
|
||||||
android:imeOptions="flagNoExtractUi"
|
|
||||||
android:inputType="number"
|
|
||||||
android:maxLength="3"
|
|
||||||
android:selectAllOnFocus="true"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:textSize="15sp"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/repeatTimesText"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:paddingStart="0dp"
|
|
||||||
android:paddingEnd="@dimen/keyline_first"
|
|
||||||
android:textAppearance="@style/TextAppearance"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</ScrollView>
|
|
||||||
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
style="@style/Widget.AppCompat.TextView.SpinnerItem"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_gravity="start"
|
|
||||||
android:paddingTop="12dp"
|
|
||||||
android:paddingBottom="12dp"
|
|
||||||
android:paddingStart="0dp"
|
|
||||||
android:paddingEnd="0dp"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:textAlignment="viewStart"
|
|
||||||
android:textColor="?attr/colorOnSecondary"/>
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!--
|
|
||||||
/* //device/apps/common/assets/res/any/layout/simple_spinner_item.xml
|
|
||||||
**
|
|
||||||
** Copyright 2008, The Android Open Source Project
|
|
||||||
**
|
|
||||||
** Licensed under the Apache License, Version 2.0 (the "License")
|
|
||||||
** you may not use this file except in compliance with the License.
|
|
||||||
** You may obtain a copy of the License at
|
|
||||||
**
|
|
||||||
** http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
**
|
|
||||||
** Unless required by applicable law or agreed to in writing, software
|
|
||||||
** distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
** See the License for the specific language governing permissions and
|
|
||||||
** limitations under the License.
|
|
||||||
*/
|
|
||||||
-->
|
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="fill_parent"
|
|
||||||
android:layout_height="?attr/dropdownListPreferredItemHeight"
|
|
||||||
android:gravity="center_vertical"
|
|
||||||
android:orientation="horizontal">
|
|
||||||
|
|
||||||
<CheckedTextView
|
|
||||||
android:id="@+id/text1"
|
|
||||||
style="?attr/spinnerDropDownItemStyle"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:ellipsize="marquee"
|
|
||||||
android:singleLine="true"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/text2"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignBaseline="@id/text1"
|
|
||||||
android:layout_alignParentEnd="true"
|
|
||||||
android:layout_toEndOf="@id/text1"
|
|
||||||
android:paddingStart="8dp"
|
|
||||||
android:paddingEnd="8dp"
|
|
||||||
android:gravity="center_vertical|end"
|
|
||||||
android:textSize="12sp"/>
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
||||||
@ -1,67 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2015 Vikram Kakkar
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
-->
|
|
||||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/weekGroup"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:gravity="center_horizontal"
|
|
||||||
android:orientation="horizontal">
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/week_day_1"
|
|
||||||
layout="@layout/week_day_button"/>
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/week_day_2"
|
|
||||||
layout="@layout/week_day_button"/>
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/week_day_3"
|
|
||||||
layout="@layout/week_day_button"/>
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/week_day_4"
|
|
||||||
layout="@layout/week_day_button"/>
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/weekGroup2"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:gravity="center_horizontal"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
tools:ignore="InconsistentLayout">
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/week_day_5"
|
|
||||||
layout="@layout/week_day_button"/>
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/week_day_6"
|
|
||||||
layout="@layout/week_day_button"/>
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/week_day_7"
|
|
||||||
layout="@layout/week_day_button"/>
|
|
||||||
|
|
||||||
<include
|
|
||||||
layout="@layout/week_day_button"
|
|
||||||
android:visibility="invisible"/>
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
</merge>
|
|
||||||
@ -1,11 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<ToggleButton
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:id="@+id/button"
|
|
||||||
android:layout_width="60dp"
|
|
||||||
android:layout_height="60dp"
|
|
||||||
android:background="@null"
|
|
||||||
android:gravity="center"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:textAllCaps="true"
|
|
||||||
android:textSize="12sp"/>
|
|
||||||
@ -0,0 +1,173 @@
|
|||||||
|
package org.tasks.repeats
|
||||||
|
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import net.fortuna.ical4j.model.Recur.Frequency.DAILY
|
||||||
|
import net.fortuna.ical4j.model.Recur.Frequency.HOURLY
|
||||||
|
import net.fortuna.ical4j.model.Recur.Frequency.MINUTELY
|
||||||
|
import net.fortuna.ical4j.model.Recur.Frequency.MONTHLY
|
||||||
|
import net.fortuna.ical4j.model.Recur.Frequency.SECONDLY
|
||||||
|
import net.fortuna.ical4j.model.Recur.Frequency.YEARLY
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Test
|
||||||
|
import org.tasks.repeats.CustomRecurrenceActivity.Companion.EXTRA_DATE
|
||||||
|
import org.tasks.repeats.CustomRecurrenceActivity.Companion.EXTRA_RRULE
|
||||||
|
import org.tasks.time.DateTime
|
||||||
|
import java.time.DayOfWeek
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class CustomRecurrenceViewModelTest {
|
||||||
|
@Test
|
||||||
|
fun defaultStateValue() {
|
||||||
|
val state = newVM().state.value
|
||||||
|
assertEquals(CustomRecurrenceViewModel.ViewState(), state)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun setFrequencies() {
|
||||||
|
assertEquals("FREQ=SECONDLY", newVM { setFrequency(SECONDLY) }.getRecur())
|
||||||
|
assertEquals("FREQ=MINUTELY", newVM { setFrequency(MINUTELY) }.getRecur())
|
||||||
|
assertEquals("FREQ=HOURLY", newVM { setFrequency(HOURLY) }.getRecur())
|
||||||
|
assertEquals("FREQ=DAILY", newVM { setFrequency(DAILY) }.getRecur())
|
||||||
|
assertEquals("FREQ=WEEKLY", newVM().getRecur())
|
||||||
|
assertEquals("FREQ=MONTHLY", newVM { setFrequency(MONTHLY) }.getRecur())
|
||||||
|
assertEquals("FREQ=YEARLY", newVM { setFrequency(YEARLY) }.getRecur())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun setInterval() {
|
||||||
|
assertEquals("FREQ=WEEKLY;INTERVAL=4", newVM { setInterval(4) }.getRecur())
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun ignoreCountWhenChangingToNever() {
|
||||||
|
assertEquals(
|
||||||
|
"FREQ=WEEKLY",
|
||||||
|
newVM("FREQ=WEEKLY;COUNT=2") { setEndType(0) }.getRecur()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun setEndDate() {
|
||||||
|
assertEquals(
|
||||||
|
"FREQ=WEEKLY;UNTIL=20230726",
|
||||||
|
newVM {
|
||||||
|
setEndDate(DateTime(2023, 7, 26).millis)
|
||||||
|
setEndType(1)
|
||||||
|
}.getRecur()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun ignoreEndDateWhenChangingToNever() {
|
||||||
|
assertEquals(
|
||||||
|
"FREQ=WEEKLY",
|
||||||
|
newVM("FREQ=WEEKLY;UNTIL=20230726") { setEndType(0) }.getRecur()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun setDaysInOrder() {
|
||||||
|
assertEquals(
|
||||||
|
"FREQ=WEEKLY;BYDAY=MO,TU,WE",
|
||||||
|
newVM {
|
||||||
|
toggleDay(DayOfWeek.MONDAY)
|
||||||
|
toggleDay(DayOfWeek.WEDNESDAY)
|
||||||
|
toggleDay(DayOfWeek.TUESDAY)
|
||||||
|
}
|
||||||
|
.getRecur()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun ignoreDaysForNonWeekly() {
|
||||||
|
assertEquals(
|
||||||
|
"FREQ=MONTHLY",
|
||||||
|
newVM {
|
||||||
|
setFrequency(MONTHLY)
|
||||||
|
toggleDay(DayOfWeek.MONDAY)
|
||||||
|
}
|
||||||
|
.getRecur()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun setCount() {
|
||||||
|
assertEquals(
|
||||||
|
"FREQ=WEEKLY;COUNT=3",
|
||||||
|
newVM {
|
||||||
|
setEndType(2)
|
||||||
|
setOccurrences(3)
|
||||||
|
}
|
||||||
|
.getRecur()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun toggleDayOff() {
|
||||||
|
assertEquals(
|
||||||
|
"FREQ=WEEKLY;BYDAY=MO",
|
||||||
|
newVM("FREQ=WEEKLY;BYDAY=MO,TU") { toggleDay(DayOfWeek.TUESDAY) }.getRecur()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun nthDayOfMonth() {
|
||||||
|
assertEquals(
|
||||||
|
"FREQ=MONTHLY;BYDAY=4TH",
|
||||||
|
newVM(dueDate = DateTime(2023, 7, 27)) {
|
||||||
|
setFrequency(MONTHLY)
|
||||||
|
setMonthSelection(1)
|
||||||
|
}.getRecur()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun lastDayOfMonth() {
|
||||||
|
assertEquals(
|
||||||
|
"FREQ=MONTHLY;BYDAY=-1TH",
|
||||||
|
newVM(dueDate = DateTime(2023, 7, 27)) {
|
||||||
|
setFrequency(MONTHLY)
|
||||||
|
setMonthSelection(2)
|
||||||
|
}.getRecur()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun restoreMonthDay() {
|
||||||
|
assertEquals(
|
||||||
|
"FREQ=MONTHLY;BYDAY=-1TH",
|
||||||
|
newVM(
|
||||||
|
recur = "FREQ=MONTHLY;BYDAY=-1TH",
|
||||||
|
dueDate = DateTime(2023, 7, 27)
|
||||||
|
).getRecur()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun changeMonthDay() {
|
||||||
|
assertEquals(
|
||||||
|
"FREQ=MONTHLY;BYDAY=4TH",
|
||||||
|
newVM(
|
||||||
|
recur = "FREQ=MONTHLY;BYDAY=-1TH",
|
||||||
|
dueDate = DateTime(2023, 7, 27)
|
||||||
|
) {
|
||||||
|
setMonthSelection(1)
|
||||||
|
}.getRecur()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun newVM(
|
||||||
|
recur: String? = null,
|
||||||
|
dueDate: DateTime = DateTime(0),
|
||||||
|
block: CustomRecurrenceViewModel.() -> Unit = {}
|
||||||
|
) =
|
||||||
|
CustomRecurrenceViewModel(
|
||||||
|
savedStateHandle = SavedStateHandle(
|
||||||
|
mapOf(
|
||||||
|
EXTRA_RRULE to recur,
|
||||||
|
EXTRA_DATE to dueDate.millis,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
locale = Locale.US
|
||||||
|
).also(block)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue