mirror of https://github.com/tasks/tasks
RepeatRow is @Composable
parent
877a2cd6a5
commit
6fd987a055
@ -0,0 +1,263 @@
|
|||||||
|
package org.tasks.repeats
|
||||||
|
|
||||||
|
/* This is mostly a copy of the BasicRecurrenceDialog with UI made @Composable */
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.BasicAlertDialog
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.RadioButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
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.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.google.common.collect.Lists
|
||||||
|
import net.fortuna.ical4j.model.Recur
|
||||||
|
import org.tasks.R
|
||||||
|
import org.tasks.analytics.Firebase
|
||||||
|
import org.tasks.compose.Constants.TextButton
|
||||||
|
import org.tasks.data.entity.Task
|
||||||
|
import org.tasks.preferences.Preferences
|
||||||
|
import org.tasks.repeats.RecurrenceUtils.newRecur
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun BasicRecurrencePicker (
|
||||||
|
dismiss: () -> Unit,
|
||||||
|
recurrence: String?,
|
||||||
|
setRecurrence: (String?) -> Unit,
|
||||||
|
peekCustomRecurrence: () -> Unit,
|
||||||
|
repeatFrom: @Task.RepeatFrom Int = Task.RepeatFrom.COMPLETION_DATE,
|
||||||
|
onRepeatFromChanged: ((@Task.RepeatFrom Int) -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
val helper = remember { RecurrenceHelper(context) }
|
||||||
|
helper.setRecurrence(recurrence)
|
||||||
|
|
||||||
|
fun setSelection(index: Int) {
|
||||||
|
when (index) {
|
||||||
|
0 -> setRecurrence(null)
|
||||||
|
5 -> {
|
||||||
|
peekCustomRecurrence()
|
||||||
|
return // to avoid dismiss() call
|
||||||
|
}
|
||||||
|
6 -> Unit
|
||||||
|
else -> {
|
||||||
|
setRecurrence(
|
||||||
|
newRecur().apply {
|
||||||
|
interval = 1
|
||||||
|
setFrequency(helper.selectedFrequency(index).name)
|
||||||
|
}.toString()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
BasicAlertDialog(
|
||||||
|
onDismissRequest = dismiss,
|
||||||
|
) {
|
||||||
|
Card {
|
||||||
|
Column (modifier = Modifier.padding(16.dp)) {
|
||||||
|
onRepeatFromChanged?.let {
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Row (modifier = Modifier.padding(start = 12.dp, top = 12.dp, bottom = 12.dp)){
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.repeats_from),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
id = if (repeatFrom == Task.RepeatFrom.COMPLETION_DATE)
|
||||||
|
R.string.repeat_type_completion
|
||||||
|
else
|
||||||
|
R.string.repeat_type_due
|
||||||
|
),
|
||||||
|
style = MaterialTheme.typography.bodyLarge.copy(
|
||||||
|
textDecoration = TextDecoration.Underline,
|
||||||
|
),
|
||||||
|
modifier = Modifier.clickable { expanded = true },
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
onClick = {
|
||||||
|
expanded = false
|
||||||
|
onRepeatFromChanged(Task.RepeatFrom.DUE_DATE)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.repeat_type_due),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
onClick = {
|
||||||
|
expanded = false
|
||||||
|
onRepeatFromChanged(Task.RepeatFrom.COMPLETION_DATE)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.repeat_type_completion),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
if (helper.isCustomValue()) {
|
||||||
|
SelectableText(
|
||||||
|
text = helper.repeatRuleToString.toString(recurrence)!!,
|
||||||
|
index = 6,
|
||||||
|
selected = 6,
|
||||||
|
setSelection = { setSelection(6) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
for (i in 0..5) {
|
||||||
|
SelectableText(
|
||||||
|
text = helper.title(i),
|
||||||
|
index = i,
|
||||||
|
selected = helper.selectionIndex(),
|
||||||
|
setSelection = { setSelection(i) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row (
|
||||||
|
modifier = Modifier.padding(bottom = 12.dp).fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End
|
||||||
|
) {
|
||||||
|
TextButton(R.string.ok, dismiss)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SelectableText (
|
||||||
|
text: String,
|
||||||
|
index: Int,
|
||||||
|
selected: Int,
|
||||||
|
setSelection: (Int) -> Unit
|
||||||
|
) {
|
||||||
|
Row (
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.clickable{ setSelection(index) }
|
||||||
|
) {
|
||||||
|
RadioButton(
|
||||||
|
selected = index == selected,
|
||||||
|
onClick = { setSelection(index) }
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
Text(text = text, textDecoration = TextDecoration.Underline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper object over recurrence string, to share basic access to rrule and to avoid reloading
|
||||||
|
* R.array.repeat_options during each recomposition
|
||||||
|
* Intended use:
|
||||||
|
*
|
||||||
|
* val helper = remember { RecurrenceHelper(context) }
|
||||||
|
* helper.setRecurrence(recurrence)
|
||||||
|
* ...
|
||||||
|
* Text(text = helper.title(selectedIndex), ...)
|
||||||
|
*/
|
||||||
|
class RecurrenceHelper (
|
||||||
|
context: Context,
|
||||||
|
) {
|
||||||
|
val repeatRuleToString = RepeatRuleToString(context,Locale.getDefault(),Firebase(context, Preferences(context)))
|
||||||
|
private var _recurrence: String? = null
|
||||||
|
val recurrence: String? get() = _recurrence
|
||||||
|
|
||||||
|
private var _rrule: Recur? = null
|
||||||
|
val rrule: Recur? get() = _rrule
|
||||||
|
|
||||||
|
private val titles: MutableList<String> =
|
||||||
|
Lists.newArrayList(*context.resources.getStringArray(R.array.repeat_options))
|
||||||
|
private val ruleTitle = if (isCustomValue()) repeatRuleToString.toString(recurrence)!! else titles[5]
|
||||||
|
|
||||||
|
fun title(index: Int): String =
|
||||||
|
if (index < 5) titles[index]
|
||||||
|
else ruleTitle
|
||||||
|
|
||||||
|
fun isCustomValue(): 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
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectionIndex(): Int =
|
||||||
|
when {
|
||||||
|
rrule == null -> 0
|
||||||
|
isCustomValue() -> 6
|
||||||
|
rrule!!.frequency == Recur.Frequency.DAILY -> 1
|
||||||
|
rrule!!.frequency == Recur.Frequency.WEEKLY -> 2
|
||||||
|
rrule!!.frequency == Recur.Frequency.MONTHLY -> 3
|
||||||
|
rrule!!.frequency == Recur.Frequency.YEARLY -> 4
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectedFrequency(index: Int): Recur.Frequency =
|
||||||
|
when (index) {
|
||||||
|
1 -> Recur.Frequency.DAILY
|
||||||
|
2 -> Recur.Frequency.WEEKLY
|
||||||
|
3 -> Recur.Frequency.MONTHLY
|
||||||
|
4 -> Recur.Frequency.YEARLY
|
||||||
|
else -> throw IllegalArgumentException()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setRecurrence(recurrence: String?) {
|
||||||
|
_recurrence = recurrence
|
||||||
|
_rrule = recurrence
|
||||||
|
.takeIf { !it.isNullOrBlank() }
|
||||||
|
?.let {
|
||||||
|
try {
|
||||||
|
newRecur(it)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun rememberRepeatRuleToString(): RepeatRuleToString {
|
||||||
|
val context = LocalContext.current
|
||||||
|
return remember { RepeatRuleToString(context,Locale.getDefault(),Firebase(context, Preferences(context))) }
|
||||||
|
}
|
||||||
@ -0,0 +1,581 @@
|
|||||||
|
package org.tasks.repeats
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is a copy of the CustomRecurrence.kt
|
||||||
|
* The function CustomRecurrence is renamed to CustomRecurrenceEdit to avoid name conflicts, and
|
||||||
|
* CustomRecurrenceEditState is used instead of the CustomRecurrenceViewModel
|
||||||
|
*/
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
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.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
|
||||||
|
import androidx.compose.material3.DisplayMode
|
||||||
|
import androidx.compose.material3.Divider
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.RadioButton
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
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.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.core.os.ConfigurationCompat
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
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.compose.pickers.DatePickerDialog
|
||||||
|
import org.tasks.kmp.org.tasks.time.getRelativeDay
|
||||||
|
import org.tasks.themes.TasksTheme
|
||||||
|
import java.time.DayOfWeek
|
||||||
|
import java.time.format.TextStyle
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun CustomRecurrenceEdit(
|
||||||
|
state: CustomRecurrenceEditState.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,
|
||||||
|
calendarDisplayMode: DisplayMode,
|
||||||
|
setDisplayMode: (DisplayMode) -> Unit,
|
||||||
|
) {
|
||||||
|
Dialog(
|
||||||
|
onDismissRequest = { discard() },
|
||||||
|
properties = DialogProperties(
|
||||||
|
dismissOnBackPress = true,
|
||||||
|
usePlatformDefaultWidth = false,
|
||||||
|
decorFitsSystemWindows = false
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
BackHandler { save() }
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
titleContentColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
navigationIconContentColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
actionIconContentColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.repeats_custom_recurrence),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = save) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
|
||||||
|
contentDescription = stringResource(id = R.string.save),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
TextButton(onClick = discard) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.cancel),
|
||||||
|
style = MaterialTheme.typography.bodyLarge.copy(
|
||||||
|
fontFeatureSettings = "c2sc, smcp"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { padding ->
|
||||||
|
Surface(
|
||||||
|
color = MaterialTheme.colorScheme.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 && !state.isMicrosoftTask) {
|
||||||
|
MonthlyPicker(
|
||||||
|
monthDay = state.monthDay,
|
||||||
|
dayNumber = state.dueDayOfMonth,
|
||||||
|
dayOfWeek = state.dueDayOfWeek,
|
||||||
|
nthWeek = state.nthWeek,
|
||||||
|
isLastWeek = state.lastWeekDayOfMonth,
|
||||||
|
locale = state.locale,
|
||||||
|
onSelected = setMonthSelection,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!state.isMicrosoftTask) {
|
||||||
|
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,
|
||||||
|
calendarDisplayMode = calendarDisplayMode,
|
||||||
|
setDisplayMode = setDisplayMode,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Header(resId: Int) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = resId),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.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.colorScheme.secondary, shape = CircleShape)
|
||||||
|
} else {
|
||||||
|
it.border(1.dp, border(), shape = CircleShape)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.clickable { toggle(dayOfWeek) },
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = string,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = if (selected.contains(dayOfWeek)) MaterialTheme.colorScheme.onSecondary else MaterialTheme.colorScheme.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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun EndsPicker(
|
||||||
|
selection: Int,
|
||||||
|
endDate: Long,
|
||||||
|
endOccurrences: Int,
|
||||||
|
calendarDisplayMode: DisplayMode,
|
||||||
|
setDisplayMode: (DisplayMode) -> Unit,
|
||||||
|
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 endDateString by remember(context, endDate) {
|
||||||
|
derivedStateOf {
|
||||||
|
runBlocking {
|
||||||
|
getRelativeDay(endDate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var showDatePicker by remember { mutableStateOf(false) }
|
||||||
|
if (showDatePicker) {
|
||||||
|
DatePickerDialog(
|
||||||
|
initialDate = endDate,
|
||||||
|
displayMode = calendarDisplayMode,
|
||||||
|
setDisplayMode = setDisplayMode,
|
||||||
|
selected = {
|
||||||
|
setEndDate(it)
|
||||||
|
showDatePicker = false
|
||||||
|
},
|
||||||
|
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
|
||||||
|
private 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()
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
private fun WeeklyPreview() {
|
||||||
|
TasksTheme {
|
||||||
|
CustomRecurrenceEdit(
|
||||||
|
state = CustomRecurrenceEditState.ViewState(frequency = Recur.Frequency.WEEKLY),
|
||||||
|
save = {},
|
||||||
|
discard = {},
|
||||||
|
setSelectedFrequency = {},
|
||||||
|
setSelectedEndType = {},
|
||||||
|
setEndDate = {},
|
||||||
|
setInterval = {},
|
||||||
|
setOccurrences = {},
|
||||||
|
toggleDay = {},
|
||||||
|
setMonthSelection = {},
|
||||||
|
calendarDisplayMode = DisplayMode.Picker,
|
||||||
|
setDisplayMode = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
private fun MonthlyPreview() {
|
||||||
|
TasksTheme {
|
||||||
|
CustomRecurrenceEdit (
|
||||||
|
state = CustomRecurrenceEditState.ViewState(frequency = Recur.Frequency.MONTHLY),
|
||||||
|
save = {},
|
||||||
|
discard = {},
|
||||||
|
setSelectedFrequency = {},
|
||||||
|
setSelectedEndType = {},
|
||||||
|
setEndDate = {},
|
||||||
|
setInterval = {},
|
||||||
|
setOccurrences = {},
|
||||||
|
toggleDay = {},
|
||||||
|
setMonthSelection = {},
|
||||||
|
calendarDisplayMode = DisplayMode.Picker,
|
||||||
|
setDisplayMode = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
private fun MinutelyPreview() {
|
||||||
|
TasksTheme {
|
||||||
|
CustomRecurrenceEdit(
|
||||||
|
state = CustomRecurrenceEditState.ViewState(frequency = Recur.Frequency.MINUTELY),
|
||||||
|
save = {},
|
||||||
|
discard = {},
|
||||||
|
setSelectedFrequency = {},
|
||||||
|
setSelectedEndType = {},
|
||||||
|
setEndDate = {},
|
||||||
|
setInterval = {},
|
||||||
|
setOccurrences = {},
|
||||||
|
toggleDay = {},
|
||||||
|
setMonthSelection = {},
|
||||||
|
calendarDisplayMode = DisplayMode.Picker,
|
||||||
|
setDisplayMode = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
private fun HourlyPreview() {
|
||||||
|
TasksTheme {
|
||||||
|
CustomRecurrenceEdit(
|
||||||
|
state = CustomRecurrenceEditState.ViewState(frequency = Recur.Frequency.HOURLY),
|
||||||
|
save = {},
|
||||||
|
discard = {},
|
||||||
|
setSelectedFrequency = {},
|
||||||
|
setSelectedEndType = {},
|
||||||
|
setEndDate = {},
|
||||||
|
setInterval = {},
|
||||||
|
setOccurrences = {},
|
||||||
|
toggleDay = {},
|
||||||
|
setMonthSelection = {},
|
||||||
|
calendarDisplayMode = DisplayMode.Picker,
|
||||||
|
setDisplayMode = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
private fun DailyPreview() {
|
||||||
|
TasksTheme {
|
||||||
|
CustomRecurrenceEdit(
|
||||||
|
state = CustomRecurrenceEditState.ViewState(frequency = Recur.Frequency.DAILY),
|
||||||
|
save = {},
|
||||||
|
discard = {},
|
||||||
|
setSelectedFrequency = {},
|
||||||
|
setSelectedEndType = {},
|
||||||
|
setEndDate = {},
|
||||||
|
setInterval = {},
|
||||||
|
setOccurrences = {},
|
||||||
|
toggleDay = {},
|
||||||
|
setMonthSelection = {},
|
||||||
|
calendarDisplayMode = DisplayMode.Picker,
|
||||||
|
setDisplayMode = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
private fun YearlyPreview() {
|
||||||
|
TasksTheme {
|
||||||
|
CustomRecurrenceEdit(
|
||||||
|
state = CustomRecurrenceEditState.ViewState(frequency = Recur.Frequency.YEARLY),
|
||||||
|
save = {},
|
||||||
|
discard = {},
|
||||||
|
setSelectedFrequency = {},
|
||||||
|
setSelectedEndType = {},
|
||||||
|
setEndDate = {},
|
||||||
|
setInterval = {},
|
||||||
|
setOccurrences = {},
|
||||||
|
toggleDay = {},
|
||||||
|
setMonthSelection = {},
|
||||||
|
calendarDisplayMode = DisplayMode.Picker,
|
||||||
|
setDisplayMode = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,271 @@
|
|||||||
|
package org.tasks.repeats
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This is essentially a copy of the CustomRecurrenceViewModel, changed to a saveable independent
|
||||||
|
* class to fit to @Composable environment
|
||||||
|
* */
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
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.data.entity.CaldavAccount.Companion.TYPE_MICROSOFT
|
||||||
|
import org.tasks.date.DateTimeUtils.toDateTime
|
||||||
|
import org.tasks.time.DateTime
|
||||||
|
import org.tasks.time.DateTimeUtils2.currentTimeMillis
|
||||||
|
import org.tasks.time.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
|
||||||
|
|
||||||
|
class CustomRecurrenceEditState(
|
||||||
|
rrule: String?,
|
||||||
|
dueDate: Long?,
|
||||||
|
val accountType: Int,
|
||||||
|
locale: Locale = Locale.getDefault()
|
||||||
|
) {
|
||||||
|
data class ViewState(
|
||||||
|
val interval: Int = 1,
|
||||||
|
val frequency: Recur.Frequency = WEEKLY,
|
||||||
|
val dueDate: Long = currentTimeMillis().startOfDay(),
|
||||||
|
val endSelection: Int = 0,
|
||||||
|
val endDate: Long = dueDate.toDateTime().plusMonths(1).startOfDay().millis,
|
||||||
|
val endCount: Int = 1,
|
||||||
|
val frequencyOptions: List<Recur.Frequency> = FREQ_ALL,
|
||||||
|
val daysOfWeek: List<DayOfWeek> = Locale.getDefault().daysOfWeek(),
|
||||||
|
val selectedDays: List<DayOfWeek> = emptyList(),
|
||||||
|
val locale: Locale = Locale.getDefault(),
|
||||||
|
val monthDay: WeekDay? = null,
|
||||||
|
val isMicrosoftTask: Boolean = false,
|
||||||
|
) {
|
||||||
|
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 = rrule
|
||||||
|
?.takeIf { it.isNotBlank() }
|
||||||
|
?.let { RRule(it) }
|
||||||
|
?.recur
|
||||||
|
val dueDate = dueDate
|
||||||
|
?.takeIf { it > 0 }
|
||||||
|
?: currentTimeMillis().startOfDay()
|
||||||
|
val isMicrosoftTask = accountType == TYPE_MICROSOFT
|
||||||
|
val frequencies = if (isMicrosoftTask) FREQ_MICROSOFT else FREQ_ALL
|
||||||
|
_state.update { state ->
|
||||||
|
state.copy(
|
||||||
|
interval = recur?.interval?.takeIf { it > 0 } ?: 1,
|
||||||
|
frequency = recur?.frequency?.takeIf { frequencies.contains(it) } ?: WEEKLY,
|
||||||
|
dueDate = dueDate,
|
||||||
|
endSelection = when {
|
||||||
|
isMicrosoftTask -> 0
|
||||||
|
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 && !isMicrosoftTask }
|
||||||
|
?.firstOrNull(),
|
||||||
|
isMicrosoftTask = isMicrosoftTask,
|
||||||
|
frequencyOptions = frequencies,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
// builder.until expects that Date() is in local timezone and strips it, which effectively
|
||||||
|
// equivalent to decrementing the "endDate" value by TimeZone.offset. This changes the date
|
||||||
|
// to the previous day in timezones to the East of GMT, so this value shall be pre-shifted
|
||||||
|
1 -> builder.until(Date(DateTime(state.endDate).let { it.millis + it.offset }))
|
||||||
|
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 FREQ_ALL = listOf(MINUTELY, HOURLY, DAILY, WEEKLY, MONTHLY, YEARLY)
|
||||||
|
val FREQ_MICROSOFT = listOf(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()
|
||||||
|
}
|
||||||
|
|
||||||
|
val Saver: Saver<CustomRecurrenceEditState, Bundle> = Saver(
|
||||||
|
save = { original: CustomRecurrenceEditState ->
|
||||||
|
Bundle().apply {
|
||||||
|
putString("rrule", original.getRecur())
|
||||||
|
putLong("dueDate", original.state.value.dueDate)
|
||||||
|
putInt("accountType", original.accountType)
|
||||||
|
putString("locale", original.state.value.locale.toLanguageTag())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
restore = { bundle ->
|
||||||
|
CustomRecurrenceEditState(
|
||||||
|
rrule = bundle.getString("rrule"),
|
||||||
|
dueDate = bundle.getLong("dueDate"),
|
||||||
|
accountType = bundle.getInt("accountType"),
|
||||||
|
locale = Locale.forLanguageTag(bundle.getString("locale"))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun rememberCustomRecurrencePickerState(
|
||||||
|
rrule: String?,
|
||||||
|
dueDate: Long?,
|
||||||
|
accountType: Int,
|
||||||
|
locale: Locale = Locale.getDefault()
|
||||||
|
): CustomRecurrenceEditState {
|
||||||
|
return rememberSaveable(saver = Saver) { CustomRecurrenceEditState(rrule, dueDate, accountType, locale) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
package org.tasks.repeats
|
||||||
|
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import org.tasks.data.entity.Task
|
||||||
|
import org.tasks.preferences.Preferences
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun RecurrencePickerDialog (
|
||||||
|
dismiss: () -> Unit,
|
||||||
|
recurrence: String?,
|
||||||
|
onRecurrenceChanged: (String?) -> Unit,
|
||||||
|
repeatFrom: @Task.RepeatFrom Int,
|
||||||
|
onRepeatFromChanged: (@Task.RepeatFrom Int) -> Unit,
|
||||||
|
accountType: Int,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val preferences = remember { Preferences(context) }
|
||||||
|
|
||||||
|
val basicDialog = remember { mutableStateOf(true) }
|
||||||
|
if (basicDialog.value) {
|
||||||
|
BasicRecurrencePicker(
|
||||||
|
dismiss = dismiss,
|
||||||
|
recurrence = recurrence,
|
||||||
|
setRecurrence = onRecurrenceChanged,
|
||||||
|
repeatFrom = repeatFrom,
|
||||||
|
onRepeatFromChanged = { onRepeatFromChanged(it) },
|
||||||
|
peekCustomRecurrence = { basicDialog.value = false },
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val state = CustomRecurrenceEditState.Companion
|
||||||
|
.rememberCustomRecurrencePickerState(
|
||||||
|
rrule = recurrence,
|
||||||
|
dueDate = null,
|
||||||
|
accountType = accountType,
|
||||||
|
locale = Locale.getDefault()
|
||||||
|
)
|
||||||
|
|
||||||
|
CustomRecurrenceEdit(
|
||||||
|
state = state.state.collectAsStateWithLifecycle().value,
|
||||||
|
save = {
|
||||||
|
onRecurrenceChanged(state.getRecur())
|
||||||
|
dismiss()
|
||||||
|
},
|
||||||
|
discard = dismiss,
|
||||||
|
setInterval = { state.setInterval(it) },
|
||||||
|
setSelectedFrequency = { state.setFrequency(it) },
|
||||||
|
setEndDate = { state.setEndDate(it) },
|
||||||
|
setSelectedEndType = { state.setEndType(it) },
|
||||||
|
setOccurrences = { state.setOccurrences(it) },
|
||||||
|
toggleDay = { state.toggleDay(it) },
|
||||||
|
setMonthSelection = { state.setMonthSelection(it) },
|
||||||
|
calendarDisplayMode = preferences.calendarDisplayMode,
|
||||||
|
setDisplayMode = { preferences.calendarDisplayMode = it }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue