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