Compose date time pickers

pull/3428/head
Alex Baker 9 months ago
parent 2f42d0ad28
commit 33a7a0a53f

@ -0,0 +1,350 @@
package org.tasks.compose.pickers
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.NextWeek
import androidx.compose.material.icons.outlined.AccessTime
import androidx.compose.material.icons.outlined.Block
import androidx.compose.material.icons.outlined.CalendarViewWeek
import androidx.compose.material.icons.outlined.Coffee
import androidx.compose.material.icons.outlined.NightsStay
import androidx.compose.material.icons.outlined.Schedule
import androidx.compose.material.icons.outlined.Today
import androidx.compose.material.icons.outlined.WbSunny
import androidx.compose.material.icons.outlined.WbTwilight
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.runBlocking
import org.tasks.R
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.dialogs.DateTimePicker.Companion.MULTIPLE_DAYS
import org.tasks.dialogs.DateTimePicker.Companion.MULTIPLE_TIMES
import org.tasks.dialogs.DateTimePicker.Companion.NO_DAY
import org.tasks.dialogs.DateTimePicker.Companion.NO_TIME
import org.tasks.dialogs.StartDatePicker.Companion.DAY_BEFORE_DUE
import org.tasks.dialogs.StartDatePicker.Companion.DUE_DATE
import org.tasks.dialogs.StartDatePicker.Companion.DUE_TIME
import org.tasks.dialogs.StartDatePicker.Companion.WEEK_BEFORE_DUE
import org.tasks.extensions.Context.is24HourFormat
import org.tasks.kmp.org.tasks.time.DateStyle
import org.tasks.kmp.org.tasks.time.getFullDate
import org.tasks.kmp.org.tasks.time.getRelativeDay
import org.tasks.kmp.org.tasks.time.getTimeString
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import org.tasks.time.minusDays
import org.tasks.time.startOfDay
import org.tasks.time.withMillisOfDay
import java.util.Calendar.FRIDAY
import java.util.Calendar.MONDAY
import java.util.Calendar.SATURDAY
import java.util.Calendar.SUNDAY
import java.util.Calendar.THURSDAY
import java.util.Calendar.TUESDAY
import java.util.Calendar.WEDNESDAY
@Composable
fun DatePickerShortcuts(
dateShortcuts: @Composable ColumnScope.() -> Unit,
timeShortcuts: @Composable ColumnScope.() -> Unit,
) {
Row(
modifier = Modifier
.padding(horizontal = 16.dp)
.fillMaxWidth(),
) {
Column(
horizontalAlignment = Alignment.Start,
) {
dateShortcuts()
}
Spacer(modifier = Modifier.weight(1f))
Column {
timeShortcuts()
}
}
}
@Composable
fun StartDateShortcuts(
selected: Long,
selectedDay: (Long) -> Unit,
selectedDayTime: (Long, Int) -> Unit,
clearDate: () -> Unit,
) {
var custom by remember { mutableLongStateOf(0) }
LaunchedEffect(selected) {
custom = if (selected !in listOf(DUE_DATE, DUE_TIME, DAY_BEFORE_DUE, WEEK_BEFORE_DUE, NO_DAY)) {
selected
} else {
custom
}
}
if (custom > 0 || custom == MULTIPLE_DAYS) {
ShortcutButton(
icon = Icons.Outlined.Today,
text = if (custom == MULTIPLE_DAYS) {
stringResource(R.string.date_picker_multiple)
} else {
remember(custom) {
runBlocking {
if (custom < currentTimeMillis().startOfDay().minusDays(1)) {
getFullDate(custom, style = DateStyle.LONG)
} else {
getRelativeDay(custom, style = DateStyle.LONG)
}
}
}
},
selected = selected == custom,
onClick = { selectedDay(custom) },
)
}
ShortcutButton(
icon = Icons.Outlined.Today,
text = stringResource(R.string.due_date),
selected = selected == DUE_DATE,
onClick = { selectedDay(DUE_DATE) },
)
ShortcutButton(
icon = Icons.Outlined.Schedule,
text = stringResource(R.string.due_time),
selected = selected == DUE_TIME,
onClick = { selectedDayTime(DUE_TIME, NO_TIME) },
)
ShortcutButton(
icon = Icons.Outlined.WbSunny,
text = stringResource(R.string.day_before_due),
selected = selected == DAY_BEFORE_DUE,
onClick = { selectedDay(DAY_BEFORE_DUE) },
)
ShortcutButton(
icon = Icons.Outlined.CalendarViewWeek,
text = stringResource(R.string.week_before_due),
selected = selected == WEEK_BEFORE_DUE,
onClick = { selectedDay(WEEK_BEFORE_DUE) },
)
ShortcutButton(
icon = Icons.Outlined.Block,
text = stringResource(R.string.no_date),
selected = selected == NO_DAY,
onClick = { clearDate() },
)
}
@Composable
fun DueDateShortcuts(
today: Long,
tomorrow: Long,
nextWeek: Long,
selected: Long,
showNoDate: Boolean,
selectedDay: (Long) -> Unit,
clearDate: () -> Unit,
) {
var custom by remember { mutableLongStateOf(0) }
LaunchedEffect(selected) {
custom = if (selected == MULTIPLE_DAYS || selected !in listOf(today, tomorrow, nextWeek, NO_DAY)) {
selected
} else {
custom
}
}
if (custom > 0 || custom == MULTIPLE_DAYS) {
ShortcutButton(
icon = Icons.Outlined.Today,
text = if (custom == MULTIPLE_DAYS) {
stringResource(R.string.date_picker_multiple)
} else {
remember(custom) {
runBlocking {
if (custom < today.minusDays(1)) {
getFullDate(custom, style = DateStyle.LONG)
} else {
getRelativeDay(custom, style = DateStyle.LONG)
}
}
}
},
selected = selected == custom,
onClick = { selectedDay(custom) },
)
}
ShortcutButton(
icon = Icons.Outlined.Today,
text = stringResource(R.string.today),
selected = selected == today,
onClick = { selectedDay(today) },
)
ShortcutButton(
icon = Icons.Outlined.WbSunny,
text = stringResource(R.string.tomorrow),
selected = selected == tomorrow,
onClick = { selectedDay(tomorrow) },
)
ShortcutButton(
icon = Icons.AutoMirrored.Outlined.NextWeek,
text = stringResource(
remember {
when (newDateTime(nextWeek).dayOfWeek) {
SUNDAY -> R.string.next_sunday
MONDAY -> R.string.next_monday
TUESDAY -> R.string.next_tuesday
WEDNESDAY -> R.string.next_wednesday
THURSDAY -> R.string.next_thursday
FRIDAY -> R.string.next_friday
SATURDAY -> R.string.next_saturday
else -> throw IllegalArgumentException()
}
}
),
selected = selected == nextWeek,
onClick = { selectedDay(nextWeek) },
)
if (showNoDate) {
ShortcutButton(
icon = Icons.Outlined.Block,
text = stringResource(R.string.no_date),
selected = selected == NO_DAY,
onClick = { clearDate() },
)
}
}
@Composable
fun TimeShortcuts(
day: Long,
selected: Int,
morning: Int,
afternoon: Int,
evening: Int,
night: Int,
selectedMillisOfDay: (Int) -> Unit,
pickTime: () -> Unit,
clearTime: () -> Unit,
) {
var custom by remember { mutableIntStateOf(0) }
LaunchedEffect(selected) {
custom = if (selected == MULTIPLE_TIMES || selected !in listOf(morning, afternoon, evening, night, NO_TIME)) {
selected
} else {
custom
}
}
val is24HourFormat = LocalContext.current.is24HourFormat
val now = remember { currentTimeMillis() }
if (custom > 0 || custom == MULTIPLE_TIMES) {
ShortcutButton(
icon = Icons.Outlined.AccessTime,
text = if (custom == MULTIPLE_TIMES) {
stringResource(R.string.date_picker_multiple)
} else {
remember(custom) {
getTimeString(now.withMillisOfDay(custom), is24HourFormat)
}
},
selected = selected == custom,
onClick = { selectedMillisOfDay(custom) },
)
}
ShortcutButton(
icon = Icons.Outlined.Coffee,
text = remember {
getTimeString(now.withMillisOfDay(morning), is24HourFormat)
},
selected = selected == morning,
onClick = { selectedMillisOfDay(morning) },
)
ShortcutButton(
icon = Icons.Outlined.WbSunny,
text = remember {
getTimeString(now.withMillisOfDay(afternoon), is24HourFormat)
},
selected = selected == afternoon,
onClick = { selectedMillisOfDay(afternoon) },
)
ShortcutButton(
icon = Icons.Outlined.WbTwilight,
text = remember {
getTimeString(now.withMillisOfDay(evening), is24HourFormat)
},
selected = selected == evening,
onClick = { selectedMillisOfDay(evening) },
)
ShortcutButton(
icon = Icons.Outlined.NightsStay,
text = remember {
getTimeString(now.withMillisOfDay(night), is24HourFormat)
},
selected = selected == night,
onClick = { selectedMillisOfDay(night) },
)
ShortcutButton(
icon = Icons.Outlined.AccessTime,
text = stringResource(R.string.shortcut_pick_time),
selected = false,
onClick = { pickTime() },
)
ShortcutButton(
icon = Icons.Outlined.Block,
text = stringResource(R.string.no_time),
selected = day != DUE_TIME && selected == NO_TIME,
onClick = { clearTime() },
)
}
@Composable
fun ShortcutButton(
icon: ImageVector,
text: String,
selected: Boolean,
onClick: () -> Unit,
) {
val color =
if (selected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface
TextButton(
onClick = { onClick() },
colors = ButtonDefaults.textButtonColors(contentColor = color)
) {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Image(
imageVector = icon,
contentDescription = null,
colorFilter = ColorFilter.tint(color)
)
Text(
text = text,
)
}
}
}

@ -0,0 +1,139 @@
package org.tasks.compose.pickers
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Keyboard
import androidx.compose.material.icons.outlined.Schedule
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TimeInput
import androidx.compose.material3.TimePicker
import androidx.compose.material3.TimePickerDefaults
import androidx.compose.material3.TimePickerLayoutType
import androidx.compose.material3.rememberTimePickerState
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.LocalConfiguration
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import org.tasks.R
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TimePickerDialog(
millisOfDay: Int,
is24Hour: Boolean,
textInput: Boolean,
selected: (Int) -> Unit,
dismiss: () -> Unit,
) {
val state = rememberTimePickerState(
initialHour = millisOfDay / (60 * 60_000),
initialMinute = (millisOfDay / (60_000)) % 60,
is24Hour = is24Hour
)
var showingTextInput by remember { mutableStateOf(textInput) }
val layoutType = with(LocalConfiguration.current) {
if (screenHeightDp < screenWidthDp) {
TimePickerLayoutType.Horizontal
} else {
TimePickerLayoutType.Vertical
}
}
BasicAlertDialog(
onDismissRequest = { dismiss() },
properties = DialogProperties(usePlatformDefaultWidth = layoutType == TimePickerLayoutType.Vertical)
) {
Surface(
shape = RoundedCornerShape(28.0.dp),
color = MaterialTheme.colorScheme.surface,
) {
Column(verticalArrangement = Arrangement.SpaceBetween) {
// Wrap the content with a Box and Modifier.weight(1f) to ensure that any "confirm"
// and "dismiss" buttons are not pushed out of view when running on small screens,
// or when nesting a DateRangePicker.
// Fill is false to support collapsing the dialog's height when switching to input
// mode.
Box(
Modifier
.fillMaxWidth()
.padding(top = 32.dp)
.weight(1f, fill = false),
contentAlignment = Alignment.Center,
) {
if (showingTextInput) {
TimeInput(
state = state,
colors = TimePickerDefaults.colors(
timeSelectorSelectedContainerColor = MaterialTheme.colorScheme.primary,
timeSelectorSelectedContentColor = MaterialTheme.colorScheme.onPrimary,
),
)
} else {
TimePicker(
state = state,
layoutType = layoutType,
colors = TimePickerDefaults.colors(
timeSelectorSelectedContainerColor = MaterialTheme.colorScheme.primary,
timeSelectorSelectedContentColor = MaterialTheme.colorScheme.onPrimary,
),
)
}
}
// Buttons
Box(
modifier = Modifier
.padding(start = 6.dp, bottom = 8.dp, end = 6.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
IconButton(
onClick = { showingTextInput = !showingTextInput },
) {
Icon(
imageVector = if (showingTextInput) {
Icons.Outlined.Schedule
} else {
Icons.Outlined.Keyboard
},
contentDescription = null
)
}
Spacer(modifier = Modifier.weight(1f))
TextButton(onClick = dismiss) {
Text(text = stringResource(id = R.string.cancel))
}
TextButton(
onClick = {
selected(state.hour * 60 * 60_000 + state.minute * 60_000)
dismiss()
}
) {
Text(text = stringResource(id = R.string.ok))
}
}
}
}
}
}
}

@ -1,47 +1,21 @@
package org.tasks.dialogs package org.tasks.dialogs
import android.app.Activity import android.app.Activity
import android.app.Dialog
import android.content.DialogInterface import android.content.DialogInterface
import android.os.Bundle import androidx.fragment.app.DialogFragment
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import android.widget.CalendarView
import android.widget.FrameLayout
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.lifecycle.lifecycleScope
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.button.MaterialButton
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.tasks.R
import org.tasks.extensions.Context.is24HourFormat
import org.tasks.kmp.org.tasks.time.getTimeString
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.themes.Theme import org.tasks.themes.Theme
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import org.tasks.time.withMillisOfDay
import javax.inject.Inject import javax.inject.Inject
abstract class BaseDateTimePicker : BottomSheetDialogFragment() { abstract class BaseDateTimePicker : DialogFragment() {
@Inject lateinit var theme: Theme @Inject lateinit var theme: Theme
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
protected var morning = 32401000
protected var afternoon = 46801000
protected var evening = 61201000
protected var night = 72001000
interface OnDismissHandler { interface OnDismissHandler {
fun onDismiss() fun onDismiss()
} }
private var onDismissHandler: OnDismissHandler? = null protected var onDismissHandler: OnDismissHandler? = null
override fun onAttach(activity: Activity) { override fun onAttach(activity: Activity) {
super.onAttach(activity) super.onAttach(activity)
@ -57,99 +31,12 @@ abstract class BaseDateTimePicker : BottomSheetDialogFragment() {
onDismissHandler?.onDismiss() onDismissHandler?.onDismiss()
} }
override fun onResume() {
super.onResume()
refreshButtons()
}
override fun onCancel(dialog: DialogInterface) = sendSelected() override fun onCancel(dialog: DialogInterface) = sendSelected()
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val dialog = super.onCreateDialog(savedInstanceState) as BottomSheetDialog
dialog.setOnShowListener {
dialog
.findViewById<FrameLayout>(com.google.android.material.R.id.design_bottom_sheet)
?.let { bottomSheet ->
val behavior = BottomSheetBehavior.from(bottomSheet)
behavior.skipCollapsed = true
val insets = ViewCompat.getRootWindowInsets(requireActivity().window.decorView)
if (insets?.isVisible(WindowInsetsCompat.Type.ime()) == true) {
lifecycleScope.launch {
delay(100)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
} else {
behavior.state = BottomSheetBehavior.STATE_EXPANDED
}
}
if (!closeAutomatically()) {
addButtons(dialog)
}
}
return dialog
}
private fun addButtons(dialog: BottomSheetDialog) {
val coordinator = dialog
.findViewById<CoordinatorLayout>(com.google.android.material.R.id.coordinator)
val containerLayout =
dialog.findViewById<FrameLayout>(com.google.android.material.R.id.container)
val buttons = theme.getLayoutInflater(requireContext())
.inflate(R.layout.dialog_date_time_picker_buttons, null)
buttons.findViewById<View>(R.id.cancel_button).setOnClickListener { dismiss() }
buttons.findViewById<View>(R.id.ok_button).setOnClickListener { sendSelected() }
buttons.layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT, Gravity.BOTTOM
).apply {
gravity = Gravity.BOTTOM
}
containerLayout!!.addView(buttons)
buttons.post {
(coordinator!!.layoutParams as ViewGroup.MarginLayoutParams).apply {
buttons.measure(
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
)
this.bottomMargin = buttons.measuredHeight
containerLayout.requestLayout()
}
}
}
protected fun closeAutomatically(): Boolean = arguments?.getBoolean(EXTRA_AUTO_CLOSE) ?: false protected fun closeAutomatically(): Boolean = arguments?.getBoolean(EXTRA_AUTO_CLOSE) ?: false
protected fun setupShortcutsAndCalendar() {
morning = preferences.dateShortcutMorning + 1000
afternoon = preferences.dateShortcutAfternoon + 1000
evening = preferences.dateShortcutEvening + 1000
night = preferences.dateShortcutNight + 1000
val is24HourFormat = requireContext().is24HourFormat
val now = currentTimeMillis()
morningButton.text = getTimeString(now.withMillisOfDay(morning), is24HourFormat)
afternoonButton.text = getTimeString(now.withMillisOfDay(afternoon), is24HourFormat)
eveningButton.text = getTimeString(now.withMillisOfDay(evening), is24HourFormat)
nightButton.text = getTimeString(now.withMillisOfDay(night), is24HourFormat)
}
protected abstract val calendarView: CalendarView
protected abstract val morningButton: MaterialButton
protected abstract val afternoonButton: MaterialButton
protected abstract val eveningButton: MaterialButton
protected abstract val nightButton: MaterialButton
protected abstract fun sendSelected() protected abstract fun sendSelected()
protected abstract fun refreshButtons()
companion object { companion object {
const val EXTRA_AUTO_CLOSE = "extra_auto_close" const val EXTRA_AUTO_CLOSE = "extra_auto_close"
} }

@ -5,38 +5,48 @@ import android.app.Activity.RESULT_OK
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isGone import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.compose.content
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import org.tasks.compose.pickers.DatePickerShortcuts
import org.tasks.R import org.tasks.compose.pickers.DueDateShortcuts
import org.tasks.compose.pickers.TimePickerDialog
import org.tasks.compose.pickers.TimeShortcuts
import org.tasks.data.createDueDate import org.tasks.data.createDueDate
import org.tasks.data.entity.Task import org.tasks.data.entity.Task
import org.tasks.databinding.DialogDateTimePickerBinding
import org.tasks.date.DateTimeUtils.newDateTime import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.date.DateTimeUtils.toDateTime import org.tasks.date.DateTimeUtils.toDateTime
import org.tasks.dialogs.MyTimePickerDialog.Companion.newTimePicker import org.tasks.dialogs.MyTimePickerDialog.Companion.timeInputMode
import org.tasks.extensions.Context.is24HourFormat import org.tasks.extensions.Context.is24HourFormat
import org.tasks.kmp.org.tasks.time.getRelativeDay
import org.tasks.kmp.org.tasks.time.getTimeString
import org.tasks.notifications.NotificationManager import org.tasks.notifications.NotificationManager
import org.tasks.themes.TasksTheme
import org.tasks.time.DateTime import org.tasks.time.DateTime
import org.tasks.time.millisOfDay import org.tasks.time.millisOfDay
import org.tasks.time.startOfDay import org.tasks.time.startOfDay
import org.tasks.time.withMillisOfDay
import java.util.Calendar.FRIDAY
import java.util.Calendar.MONDAY
import java.util.Calendar.SATURDAY
import java.util.Calendar.SUNDAY
import java.util.Calendar.THURSDAY
import java.util.Calendar.TUESDAY
import java.util.Calendar.WEDNESDAY
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -46,19 +56,9 @@ class DateTimePicker : BaseDateTimePicker() {
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao
@Inject lateinit var notificationManager: NotificationManager @Inject lateinit var notificationManager: NotificationManager
lateinit var binding: DialogDateTimePickerBinding private var selectedDay by mutableLongStateOf(NO_DAY)
private var customDate = NO_DAY private var selectedTime by mutableIntStateOf(NO_TIME)
private var customTime = NO_TIME
private var selectedDay = NO_DAY
private var selectedTime = NO_TIME
private val today = newDateTime().startOfDay() private val today = newDateTime().startOfDay()
private val tomorrow = today.plusDays(1)
private val nextWeek = today.plusDays(7)
override val calendarView get() = binding.calendarView
override val morningButton get() = binding.shortcuts.morningButton
override val afternoonButton get() = binding.shortcuts.afternoonButton
override val eveningButton get() = binding.shortcuts.eveningButton
override val nightButton get() = binding.shortcuts.nightButton
companion object { companion object {
const val EXTRA_DAY = "extra_day" const val EXTRA_DAY = "extra_day"
@ -66,16 +66,14 @@ class DateTimePicker : BaseDateTimePicker() {
const val EXTRA_TASKS = "extra_tasks" const val EXTRA_TASKS = "extra_tasks"
const val EXTRA_TIMESTAMP = "extra_timestamp" const val EXTRA_TIMESTAMP = "extra_timestamp"
const val EXTRA_HIDE_NO_DATE = "extra_hide_no_date" const val EXTRA_HIDE_NO_DATE = "extra_hide_no_date"
private const val REQUEST_TIME = 10101 const val NO_DAY = 0L
private const val FRAG_TAG_TIME_PICKER = "frag_tag_time_picker" const val NO_TIME = 0
private const val NO_DAY = 0L const val MULTIPLE_DAYS = -1L
private const val NO_TIME = 0 const val MULTIPLE_TIMES = -1
private const val MULTIPLE_DAYS = -1L
private const val MULTIPLE_TIMES = -1
fun newDateTimePicker( fun newDateTimePicker(
autoClose: Boolean, autoClose: Boolean,
vararg tasks: Task vararg tasks: Task,
): DateTimePicker { ): DateTimePicker {
val fragment = DateTimePicker() val fragment = DateTimePicker()
val dueDates = tasks.map { it.dueDate.startOfDay() }.toSet() val dueDates = tasks.map { it.dueDate.startOfDay() }.toSet()
@ -91,11 +89,11 @@ class DateTimePicker : BaseDateTimePicker() {
} }
fun newDateTimePicker( fun newDateTimePicker(
target: Fragment, target: Fragment,
rc: Int, rc: Int,
current: Long, current: Long,
autoClose: Boolean, autoClose: Boolean,
hideNoDate: Boolean, hideNoDate: Boolean,
): DateTimePicker { ): DateTimePicker {
val fragment = DateTimePicker() val fragment = DateTimePicker()
fragment.arguments = Bundle().apply { fragment.arguments = Bundle().apply {
@ -109,126 +107,117 @@ class DateTimePicker : BaseDateTimePicker() {
} }
} }
override fun onCreateView( override fun onCreate(savedInstanceState: Bundle?) {
inflater: LayoutInflater, super.onCreate(savedInstanceState)
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = DialogDateTimePickerBinding.inflate(theme.getLayoutInflater(requireContext()))
setupShortcutsAndCalendar()
with (binding.shortcuts) {
nextWeekButton.text =
getString(
when (newDateTime().plusWeeks(1).dayOfWeek) {
SUNDAY -> R.string.next_sunday
MONDAY -> R.string.next_monday
TUESDAY -> R.string.next_tuesday
WEDNESDAY -> R.string.next_wednesday
THURSDAY -> R.string.next_thursday
FRIDAY -> R.string.next_friday
SATURDAY -> R.string.next_saturday
else -> throw IllegalArgumentException()
}
)
noDateButton.isGone = requireArguments().getBoolean(EXTRA_HIDE_NO_DATE, false)
noDateButton.setOnClickListener { clearDate() }
noTime.setOnClickListener { clearTime() }
todayButton.setOnClickListener { setToday() }
tomorrowButton.setOnClickListener { setTomorrow() }
nextWeekButton.setOnClickListener { setNextWeek() }
morningButton.setOnClickListener { setMorning() }
afternoonButton.setOnClickListener { setAfternoon() }
eveningButton.setOnClickListener { setEvening() }
nightButton.setOnClickListener { setNight() }
currentDateSelection.setOnClickListener { currentDate() }
currentTimeSelection.setOnClickListener { currentTime() }
pickTimeButton.setOnClickListener { pickTime() }
}
binding.calendarView.setOnDateChangeListener { _, y, m, d ->
returnDate(day = DateTime(y, m + 1, d).millis)
refreshButtons()
}
selectedDay = savedInstanceState?.getLong(EXTRA_DAY) ?: requireArguments().getLong(EXTRA_DAY)
selectedTime =
savedInstanceState?.getInt(EXTRA_TIME)
?: requireArguments().getInt(EXTRA_TIME)
.takeIf { it == MULTIPLE_TIMES || Task.hasDueTime(it.toLong()) }
?: NO_TIME
return binding.root selectedDay =
savedInstanceState?.getLong(EXTRA_DAY) ?: requireArguments().getLong(EXTRA_DAY)
selectedTime =
savedInstanceState?.getInt(EXTRA_TIME)
?: requireArguments().getInt(EXTRA_TIME)
.takeIf { it == MULTIPLE_TIMES || Task.hasDueTime(it.toLong()) }
?: NO_TIME
} }
override fun refreshButtons() { @OptIn(ExperimentalMaterial3Api::class)
when (selectedDay) { override fun onCreateView(
0L -> binding.shortcuts.dateGroup.check(R.id.no_date_button) inflater: LayoutInflater,
today.millis -> binding.shortcuts.dateGroup.check(R.id.today_button) container: ViewGroup?,
tomorrow.millis -> binding.shortcuts.dateGroup.check(R.id.tomorrow_button) savedInstanceState: Bundle?,
nextWeek.millis -> binding.shortcuts.dateGroup.check(R.id.next_week_button) ) = content {
else -> { TasksTheme(theme = theme.themeBase.index) {
customDate = selectedDay val sheetState = rememberModalBottomSheetState(
binding.shortcuts.dateGroup.check(R.id.current_date_selection) skipPartiallyExpanded = true,
binding.shortcuts.currentDateSelection.visibility = View.VISIBLE )
binding.shortcuts.currentDateSelection.text = if (customDate == MULTIPLE_DAYS) { ModalBottomSheet(
requireContext().getString(R.string.date_picker_multiple) sheetState = sheetState,
} else { onDismissRequest = {
runBlocking { onDismissHandler?.onDismiss() ?: dismiss()
getRelativeDay(selectedDay) },
containerColor = MaterialTheme.colorScheme.surface,
) {
val state = rememberDatePickerState()
Column(
modifier = Modifier.verticalScroll(rememberScrollState())
) {
DatePicker(
state = state,
showModeToggle = false,
title = {},
headline = {
DatePickerShortcuts(
timeShortcuts = {
var showTimePicker by rememberSaveable { mutableStateOf(false) }
if (showTimePicker) {
val time = if (selectedTime == MULTIPLE_TIMES
|| !Task.hasDueTime(today.withMillisOfDay(selectedTime).millis)
) {
today.noon().millisOfDay
} else {
selectedTime
}
TimePickerDialog(
millisOfDay = time,
is24Hour = remember { requireContext().is24HourFormat },
textInput = remember { preferences.timeInputMode == 1 },
selected = { returnSelectedTime(it + 1000) },
dismiss = { showTimePicker = false },
)
}
TimeShortcuts(
day = 0,
selected = selectedTime,
morning = remember { preferences.dateShortcutMorning + 1000 },
afternoon = remember { preferences.dateShortcutAfternoon + 1000 },
evening = remember { preferences.dateShortcutEvening + 1000 },
night = remember { preferences.dateShortcutNight + 1000 },
selectedMillisOfDay = { returnSelectedTime(it) },
pickTime = { showTimePicker = true },
clearTime = { returnDate(time = 0) },
)
},
dateShortcuts = {
DueDateShortcuts(
today = today.millis,
tomorrow = remember { today.plusDays(1).millis },
nextWeek = remember { today.plusDays(7).millis },
selected = selectedDay,
showNoDate = remember {
!requireArguments().getBoolean(
EXTRA_HIDE_NO_DATE,
false
)
},
selectedDay = { returnDate(it.startOfDay()) },
clearDate = { returnDate(day = 0, time = 0) },
)
},
)
},
colors = DatePickerDefaults.colors(
containerColor = MaterialTheme.colorScheme.surface,
),
)
}
LaunchedEffect(selectedDay) {
if (selectedDay > 0) {
state.selectedDateMillis = selectedDay + (DateTime(selectedDay).offset)
} else {
state.selectedDateMillis = null
} }
} }
} LaunchedEffect(state.selectedDateMillis) {
} if (state.selectedDateMillis == selectedDay + (DateTime(selectedDay).offset)) {
if (selectedTime == MULTIPLE_TIMES || Task.hasDueTime(selectedTime.toLong())) { return@LaunchedEffect
when (selectedTime) { }
morning -> binding.shortcuts.timeGroup.check(R.id.morning_button) state.selectedDateMillis?.let {
afternoon -> binding.shortcuts.timeGroup.check(R.id.afternoon_button) returnDate(day = it - DateTime(it).offset)
evening -> binding.shortcuts.timeGroup.check(R.id.evening_button) }
night -> binding.shortcuts.timeGroup.check(R.id.night_button)
else -> {
customTime = selectedTime
binding.shortcuts.timeGroup.check(R.id.current_time_selection)
binding.shortcuts.currentTimeSelection.visibility = View.VISIBLE
binding.shortcuts.currentTimeSelection.text =
if (customTime == MULTIPLE_TIMES) {
requireContext().getString(R.string.date_picker_multiple)
} else {
getTimeString(
today.millis.withMillisOfDay(selectedTime),
requireContext().is24HourFormat
)
}
} }
} }
} else {
binding.shortcuts.timeGroup.check(R.id.no_time)
}
if (selectedDay > 0) {
binding.calendarView.setDate(selectedDay, true, true)
} }
} }
private fun clearDate() = returnDate(day = 0, time = 0)
private fun clearTime() = returnDate(time = 0)
private fun setToday() = returnDate(day = today.startOfDay().millis)
private fun setTomorrow() = returnDate(day = tomorrow.startOfDay().millis)
private fun setNextWeek() = returnDate(day = nextWeek.startOfDay().millis)
private fun setMorning() = returnSelectedTime(morning)
private fun setAfternoon() = returnSelectedTime(afternoon)
private fun setEvening() = returnSelectedTime(evening)
private fun setNight() = returnSelectedTime(night)
private fun currentDate() = returnDate(day = customDate)
private fun currentTime() = returnSelectedTime(customTime)
private fun pickTime() {
val time = if (selectedTime == MULTIPLE_TIMES
|| !Task.hasDueTime(today.withMillisOfDay(selectedTime).millis)) {
today.noon().millisOfDay
} else {
selectedTime
}
newTimePicker(this, REQUEST_TIME, today.withMillisOfDay(time).millis)
.show(parentFragmentManager, FRAG_TAG_TIME_PICKER)
}
private fun returnSelectedTime(millisOfDay: Int) { private fun returnSelectedTime(millisOfDay: Int) {
val day = when { val day = when {
selectedDay == MULTIPLE_DAYS -> MULTIPLE_DAYS selectedDay == MULTIPLE_DAYS -> MULTIPLE_DAYS
@ -244,8 +233,6 @@ class DateTimePicker : BaseDateTimePicker() {
selectedTime = time selectedTime = time
if (closeAutomatically()) { if (closeAutomatically()) {
sendSelected() sendSelected()
} else {
refreshButtons()
} }
} }
@ -254,43 +241,49 @@ class DateTimePicker : BaseDateTimePicker() {
override fun sendSelected() { override fun sendSelected() {
if (selectedDay != arguments?.getLong(EXTRA_DAY) if (selectedDay != arguments?.getLong(EXTRA_DAY)
|| selectedTime != arguments?.getInt(EXTRA_TIME)) { || selectedTime != arguments?.getInt(EXTRA_TIME)
) {
if (taskIds.isEmpty()) { if (taskIds.isEmpty()) {
val intent = Intent() val intent = Intent()
intent.putExtra(EXTRA_TIMESTAMP, when { intent.putExtra(
selectedDay == NO_DAY -> 0 EXTRA_TIMESTAMP, when {
selectedTime == NO_TIME -> selectedDay selectedDay == NO_DAY -> 0
else -> selectedDay.toDateTime().withMillisOfDay(selectedTime).millis selectedTime == NO_TIME -> selectedDay
}) else -> selectedDay.toDateTime().withMillisOfDay(selectedTime).millis
}
)
targetFragment?.onActivityResult(targetRequestCode, RESULT_OK, intent) targetFragment?.onActivityResult(targetRequestCode, RESULT_OK, intent)
} else { } else {
lifecycleScope.launch(NonCancellable) { lifecycleScope.launch(NonCancellable) {
taskDao taskDao
.fetch(taskIds.toList()) .fetch(taskIds.toList())
.forEach { .forEach {
val day = if (selectedDay == MULTIPLE_DAYS) { val day = if (selectedDay == MULTIPLE_DAYS) {
if (it.hasDueDate()) it.dueDate else today.millis if (it.hasDueDate()) it.dueDate else today.millis
} else { } else {
selectedDay selectedDay
} }
val time = if (selectedTime == MULTIPLE_TIMES) { val time = if (selectedTime == MULTIPLE_TIMES) {
if (it.hasDueTime()) it.dueDate.millisOfDay else NO_TIME if (it.hasDueTime()) it.dueDate.millisOfDay else NO_TIME
} else { } else {
selectedTime selectedTime
} }
it.setDueDateAdjustingHideUntil(when { it.setDueDateAdjustingHideUntil(
when {
day == NO_DAY -> 0L day == NO_DAY -> 0L
time == NO_TIME -> createDueDate( time == NO_TIME -> createDueDate(
Task.URGENCY_SPECIFIC_DAY, Task.URGENCY_SPECIFIC_DAY,
day day
) )
else -> createDueDate( else -> createDueDate(
Task.URGENCY_SPECIFIC_DAY_TIME, Task.URGENCY_SPECIFIC_DAY_TIME,
day.toDateTime().withMillisOfDay(time).millis day.toDateTime().withMillisOfDay(time).millis
) )
}) }
taskDao.save(it) )
} taskDao.save(it)
}
} }
} }
} }
@ -303,17 +296,4 @@ class DateTimePicker : BaseDateTimePicker() {
outState.putLong(EXTRA_DAY, selectedDay) outState.putLong(EXTRA_DAY, selectedDay)
outState.putInt(EXTRA_TIME, selectedTime) outState.putInt(EXTRA_TIME, selectedTime)
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_TIME) {
if (resultCode == RESULT_OK) {
val timestamp = data!!.getLongExtra(MyTimePickerDialog.EXTRA_TIMESTAMP, today.millis)
returnSelectedTime(newDateTime(timestamp).millisOfDay + 1000)
} else {
refreshButtons()
}
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}
} }

@ -15,7 +15,6 @@ import kotlinx.coroutines.launch
import org.tasks.R import org.tasks.R
import org.tasks.compose.edit.Priority import org.tasks.compose.edit.Priority
import org.tasks.data.entity.Task import org.tasks.data.entity.Task
import org.tasks.databinding.DialogDateTimePickerBinding
import org.tasks.databinding.DialogPriorityPickerBinding import org.tasks.databinding.DialogPriorityPickerBinding
import javax.inject.Inject import javax.inject.Inject
@ -39,7 +38,6 @@ class PriorityPicker : DialogFragment() {
} }
} }
lateinit var binding: DialogDateTimePickerBinding
private val priorityPickerViewModel: PriorityPickerViewModel by viewModels() private val priorityPickerViewModel: PriorityPickerViewModel by viewModels()
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {

@ -5,23 +5,41 @@ import android.app.Activity.RESULT_OK
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.DatePicker
import androidx.compose.material3.DatePickerDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.rememberDatePickerState
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.compose.content
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.runBlocking import org.tasks.compose.pickers.DatePickerShortcuts
import org.tasks.R import org.tasks.compose.pickers.StartDateShortcuts
import org.tasks.compose.pickers.TimePickerDialog
import org.tasks.compose.pickers.TimeShortcuts
import org.tasks.data.entity.Task import org.tasks.data.entity.Task
import org.tasks.databinding.DialogStartDatePickerBinding
import org.tasks.date.DateTimeUtils.newDateTime import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.dialogs.MyTimePickerDialog.Companion.newTimePicker import org.tasks.dialogs.MyTimePickerDialog.Companion.timeInputMode
import org.tasks.extensions.Context.is24HourFormat import org.tasks.extensions.Context.is24HourFormat
import org.tasks.kmp.org.tasks.time.getRelativeDay
import org.tasks.kmp.org.tasks.time.getTimeString
import org.tasks.notifications.NotificationManager import org.tasks.notifications.NotificationManager
import org.tasks.themes.TasksTheme
import org.tasks.time.DateTime import org.tasks.time.DateTime
import org.tasks.time.withMillisOfDay
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
@ -31,21 +49,11 @@ class StartDatePicker : BaseDateTimePicker() {
@Inject lateinit var taskDao: TaskDao @Inject lateinit var taskDao: TaskDao
@Inject lateinit var notificationManager: NotificationManager @Inject lateinit var notificationManager: NotificationManager
lateinit var binding: DialogStartDatePickerBinding private var selectedDay by mutableLongStateOf(NO_DAY)
private var customDate = NO_DAY private var selectedTime by mutableIntStateOf(NO_TIME)
private var customTime = NO_TIME
private var selectedDay = NO_DAY
private var selectedTime = NO_TIME
private val today = newDateTime().startOfDay() private val today = newDateTime().startOfDay()
override val calendarView get() = binding.calendarView
override val morningButton get() = binding.shortcuts.morningButton
override val afternoonButton get() = binding.shortcuts.afternoonButton
override val eveningButton get() = binding.shortcuts.eveningButton
override val nightButton get() = binding.shortcuts.nightButton
companion object { companion object {
private const val REQUEST_TIME = 10101
private const val FRAG_TAG_TIME_PICKER = "frag_tag_time_picker"
const val EXTRA_DAY = "extra_day" const val EXTRA_DAY = "extra_day"
const val EXTRA_TIME = "extra_time" const val EXTRA_TIME = "extra_time"
const val NO_DAY = 0L const val NO_DAY = 0L
@ -67,110 +75,116 @@ class StartDatePicker : BaseDateTimePicker() {
} }
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreate(savedInstanceState: Bundle?) {
binding = DialogStartDatePickerBinding.inflate(theme.getLayoutInflater(requireContext())) super.onCreate(savedInstanceState)
setupShortcutsAndCalendar()
binding.calendarView.setOnDateChangeListener { _, y, m, d ->
returnDate(day = DateTime(y, m + 1, d).millis)
refreshButtons()
}
selectedDay = savedInstanceState?.getLong(EXTRA_DAY) ?: requireArguments().getLong(EXTRA_DAY) selectedDay = savedInstanceState?.getLong(EXTRA_DAY) ?: requireArguments().getLong(EXTRA_DAY)
selectedTime = selectedTime =
savedInstanceState?.getInt(EXTRA_TIME) savedInstanceState?.getInt(EXTRA_TIME)
?: requireArguments().getInt(EXTRA_TIME) ?: requireArguments().getInt(EXTRA_TIME)
.takeIf { Task.hasDueTime(it.toLong()) } .takeIf { Task.hasDueTime(it.toLong()) }
?: NO_TIME ?: NO_TIME
with(binding.shortcuts) {
noDateButton.setOnClickListener { clearDate() }
noTime.setOnClickListener { clearTime() }
dueDateButton.setOnClickListener { setToday() }
dayBeforeDueButton.setOnClickListener { setTomorrow() }
weekBeforeDueButton.setOnClickListener { setNextWeek() }
morningButton.setOnClickListener { setMorning() }
afternoonButton.setOnClickListener { setAfternoon() }
eveningButton.setOnClickListener { setEvening() }
nightButton.setOnClickListener { setNight() }
dueTimeButton.setOnClickListener { setDueTime() }
currentDateSelection.setOnClickListener { currentDate() }
currentTimeSelection.setOnClickListener { currentTime() }
pickTimeButton.setOnClickListener { pickTime() }
}
return binding.root
} }
override fun refreshButtons() { @OptIn(ExperimentalMaterial3Api::class)
when (selectedDay) { override fun onCreateView(
0L -> binding.shortcuts.dateGroup.check(R.id.no_date_button) inflater: LayoutInflater,
DUE_DATE -> binding.shortcuts.dateGroup.check(R.id.due_date_button) container: ViewGroup?,
DUE_TIME -> { savedInstanceState: Bundle?
binding.shortcuts.dateGroup.check(R.id.due_time_button) ) = content {
binding.shortcuts.timeGroup.clearChecked() TasksTheme(theme = theme.themeBase.index) {
} val sheetState = rememberModalBottomSheetState(
DAY_BEFORE_DUE -> binding.shortcuts.dateGroup.check(R.id.day_before_due_button) skipPartiallyExpanded = true,
WEEK_BEFORE_DUE -> binding.shortcuts.dateGroup.check(R.id.week_before_due_button) )
else -> { ModalBottomSheet(
customDate = selectedDay sheetState = sheetState,
binding.shortcuts.dateGroup.check(R.id.current_date_selection) onDismissRequest = {
binding.shortcuts.currentDateSelection.visibility = View.VISIBLE onDismissHandler?.onDismiss() ?: dismiss()
binding.shortcuts.currentDateSelection.text = },
runBlocking { containerColor = MaterialTheme.colorScheme.surface,
getRelativeDay(selectedDay) ) {
} val state = rememberDatePickerState()
} Column(
} modifier = Modifier.verticalScroll(rememberScrollState()),
if (Task.hasDueTime(selectedTime.toLong())) { ) {
when (selectedTime) { DatePicker(
morning -> binding.shortcuts.timeGroup.check(R.id.morning_button) state = state,
afternoon -> binding.shortcuts.timeGroup.check(R.id.afternoon_button) showModeToggle = false,
evening -> binding.shortcuts.timeGroup.check(R.id.evening_button) title = {},
night -> binding.shortcuts.timeGroup.check(R.id.night_button) headline = {
else -> { DatePickerShortcuts(
customTime = selectedTime timeShortcuts = {
binding.shortcuts.timeGroup.check(R.id.current_time_selection) var showTimePicker by rememberSaveable { mutableStateOf(false) }
binding.shortcuts.currentTimeSelection.visibility = View.VISIBLE if (showTimePicker) {
binding.shortcuts.currentTimeSelection.text = getTimeString( val time = if (selectedTime < 0 || !Task.hasDueTime(
today.millis.withMillisOfDay(selectedTime), today.withMillisOfDay(selectedTime).millis
requireContext().is24HourFormat )
) {
today.noon().millisOfDay
} else {
selectedTime
}
TimePickerDialog(
millisOfDay = time,
is24Hour = remember { requireContext().is24HourFormat },
textInput = remember { preferences.timeInputMode == 1 },
selected = { returnSelectedTime(it + 1000) },
dismiss = { showTimePicker = false }
)
}
TimeShortcuts(
day = selectedDay,
selected = selectedTime,
morning = remember { preferences.dateShortcutMorning + 1000 },
afternoon = remember { preferences.dateShortcutAfternoon + 1000 },
evening = remember { preferences.dateShortcutEvening + 1000 },
night = remember { preferences.dateShortcutNight + 1000 },
selectedMillisOfDay = { returnSelectedTime(it) },
pickTime = { showTimePicker = true },
clearTime = {
returnDate(
day = when (selectedDay) {
DUE_TIME -> DUE_DATE
else -> selectedDay
},
time = 0
)
},
)
},
dateShortcuts = {
StartDateShortcuts(
selected = selectedDay,
selectedDay = { returnDate(it) },
selectedDayTime = { day, time -> returnDate(day, time) },
clearDate = { returnDate(day = 0, time = 0) },
)
},
)
},
colors = DatePickerDefaults.colors(
containerColor = MaterialTheme.colorScheme.surface,
),
) )
LaunchedEffect(selectedDay) {
if (selectedDay > 0) {
state.selectedDateMillis = selectedDay + (DateTime(selectedDay).offset)
} else {
state.selectedDateMillis = null
}
}
LaunchedEffect(state.selectedDateMillis) {
if (state.selectedDateMillis == selectedDay + (DateTime(selectedDay).offset)) {
return@LaunchedEffect
}
state.selectedDateMillis?.let {
returnDate(day = it - DateTime(it).offset)
}
}
} }
} }
if (selectedDay == DUE_TIME) {
selectedDay = DUE_DATE
}
} else if (selectedDay != DUE_TIME) {
binding.shortcuts.timeGroup.check(R.id.no_time)
}
if (selectedDay > 0) {
binding.calendarView.setDate(selectedDay, true, true)
}
}
private fun clearDate() = returnDate(day = 0, time = 0)
private fun clearTime() = returnDate(
day = when (selectedDay) {
DUE_TIME -> DUE_DATE
else -> selectedDay
},
time = 0
)
private fun setToday() = returnDate(day = DUE_DATE)
private fun setTomorrow() = returnDate(day = DAY_BEFORE_DUE)
private fun setNextWeek() = returnDate(day = WEEK_BEFORE_DUE)
private fun setMorning() = returnSelectedTime(morning)
private fun setAfternoon() = returnSelectedTime(afternoon)
private fun setEvening() = returnSelectedTime(evening)
private fun setNight() = returnSelectedTime(night)
private fun setDueTime() = returnDate(day = DUE_TIME, time = NO_TIME)
private fun currentDate() = returnDate(day = customDate)
private fun currentTime() = returnSelectedTime(customTime)
private fun pickTime() {
val time = if (selectedTime < 0 || !Task.hasDueTime(today.withMillisOfDay(selectedTime).millis)) {
today.noon().millisOfDay
} else {
selectedTime
} }
newTimePicker(this, REQUEST_TIME, today.withMillisOfDay(time).millis)
.show(parentFragmentManager, FRAG_TAG_TIME_PICKER)
} }
private fun returnSelectedTime(millisOfDay: Int) { private fun returnSelectedTime(millisOfDay: Int) {
@ -188,8 +202,6 @@ class StartDatePicker : BaseDateTimePicker() {
selectedTime = time selectedTime = time
if (closeAutomatically()) { if (closeAutomatically()) {
sendSelected() sendSelected()
} else {
refreshButtons()
} }
} }
@ -211,17 +223,4 @@ class StartDatePicker : BaseDateTimePicker() {
outState.putLong(EXTRA_DAY, selectedDay) outState.putLong(EXTRA_DAY, selectedDay)
outState.putInt(EXTRA_TIME, selectedTime) outState.putInt(EXTRA_TIME, selectedTime)
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_TIME) {
if (resultCode == RESULT_OK) {
val timestamp = data!!.getLongExtra(MyTimePickerDialog.EXTRA_TIMESTAMP, today.millis)
returnSelectedTime(newDateTime(timestamp).millisOfDay + 1000)
} else {
refreshButtons()
}
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}
} }

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:alpha="1.00" android:color="?attr/colorAccent" android:state_checkable="true" android:state_checked="true" android:state_enabled="true"/>
<item android:alpha="0.60" android:color="?attr/colorOnSurface" android:state_checkable="true" android:state_checked="false" android:state_enabled="true"/>
<item android:alpha="1.00" android:color="?attr/colorAccent" android:state_enabled="true"/>
<item android:alpha="0.38" android:color="?attr/colorOnSurface"/>
</selector>

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (C) 2020 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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?attr/colorOnSecondary" android:state_selected="true" />
<item android:color="?attr/colorOnSurface" />
</selector>

@ -1,148 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/date_group"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/half_keyline_first"
android:layout_marginBottom="@dimen/half_keyline_first"
android:orientation="vertical"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintRight_toLeftOf="@id/guideline"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_max="wrap"
app:layout_constraintWidth_percent="0.5"
app:singleSelection="true"
android:layout_marginStart="16dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/current_date_selection"
style="@style/DateTimeShortcuts"
tools:text="Nov 24, 2021"
android:visibility="gone"
app:icon="@drawable/ic_outline_today_24px"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/today_button"
style="@style/DateTimeShortcuts"
android:text="@string/today"
app:icon="@drawable/ic_calendar_today_24px"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/tomorrow_button"
style="@style/DateTimeShortcuts"
android:text="@string/tomorrow"
app:icon="@drawable/ic_outline_wb_sunny_24px"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/today_button" />
<com.google.android.material.button.MaterialButton
android:id="@+id/next_week_button"
style="@style/DateTimeShortcuts"
tools:text="Next Thurs"
app:icon="@drawable/ic_next_week_24px"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tomorrow_button" />
<com.google.android.material.button.MaterialButton
android:id="@+id/no_date_button"
style="@style/DateTimeShortcuts"
android:text="@string/no_date"
app:icon="@drawable/ic_outline_not_interested_24px"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/next_week_button" />
</com.google.android.material.button.MaterialButtonToggleGroup>
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
<com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/time_group"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/half_keyline_first"
android:layout_marginBottom="@dimen/half_keyline_first"
android:orientation="vertical"
app:layout_constraintLeft_toRightOf="@id/guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_max="wrap"
app:layout_constraintWidth_percent="0.5"
app:selectionRequired="true"
app:singleSelection="true"
android:layout_marginEnd="16dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/current_time_selection"
style="@style/DateTimeShortcuts"
app:icon="@drawable/ic_outline_schedule_24px"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="08:15" />
<com.google.android.material.button.MaterialButton
android:id="@+id/morning_button"
style="@style/DateTimeShortcuts"
tools:text="9 AM"
app:icon="@drawable/ic_local_cafe_24px"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/afternoon_button"
style="@style/DateTimeShortcuts"
android:layout_marginTop="1dp"
tools:text="1 PM"
app:icon="@drawable/ic_outline_wb_sunny_24px"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/morning_button" />
<com.google.android.material.button.MaterialButton
android:id="@+id/evening_button"
style="@style/DateTimeShortcuts"
tools:text="5 PM"
app:icon="@drawable/ic_weather_sunset"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/afternoon_button" />
<com.google.android.material.button.MaterialButton
android:id="@+id/night_button"
style="@style/DateTimeShortcuts"
tools:text="8 PM"
app:icon="@drawable/ic_nights_stay_24px"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/evening_button" />
<com.google.android.material.button.MaterialButton
android:id="@+id/pick_time_button"
style="@style/DateTimeShortcuts"
android:text="@string/shortcut_pick_time"
app:icon="@drawable/ic_outline_schedule_24px"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/evening_button" />
<com.google.android.material.button.MaterialButton
android:id="@+id/no_time"
style="@style/DateTimeShortcuts"
android:text="@string/no_time"
app:icon="@drawable/ic_outline_not_interested_24px"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/night_button" />
</com.google.android.material.button.MaterialButtonToggleGroup>
</androidx.constraintlayout.widget.ConstraintLayout>

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_behavior="android.support.design.widget.BottomSheetBehavior">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/dialog_background"
android:paddingBottom="@dimen/keyline_first">
<include
layout="@layout/date_time_picker_shortcuts"
android:layout_alignParentTop="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/shortcuts" />
<CalendarView
android:id="@+id/calendar_view"
android:layout_below="@id/shortcuts"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</RelativeLayout>
</androidx.core.widget.NestedScrollView>

@ -1,43 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/dialog_background">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end">
<View
style="@style/horizontal_divider"
android:id="@+id/divider"
android:background="@color/divider"
android:layout_alignParentTop="true" />
<com.google.android.material.button.MaterialButton
style="@style/Widget.Material3.Button.TextButton"
android:textColor="?attr/colorAccent"
android:id="@+id/ok_button"
android:text="@string/ok"
android:layout_below="@id/divider"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true" />
<com.google.android.material.button.MaterialButton
style="@style/Widget.Material3.Button.TextButton"
android:textColor="?attr/colorAccent"
android:layout_below="@id/divider"
android:id="@+id/cancel_button"
android:text="@string/cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingStart="@dimen/keyline_first"
android:paddingEnd="@dimen/keyline_first"
android:layout_toStartOf="@id/ok_button" />
</RelativeLayout>
</FrameLayout>

@ -1,28 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_behavior="android.support.design.widget.BottomSheetBehavior">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/keyline_first">
<include
layout="@layout/start_date_picker_shortcuts"
android:layout_alignParentTop="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/shortcuts" />
<CalendarView
android:id="@+id/calendar_view"
android:layout_below="@id/shortcuts"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</RelativeLayout>
</androidx.core.widget.NestedScrollView>

@ -1,149 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/date_group"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/half_keyline_first"
android:layout_marginBottom="@dimen/half_keyline_first"
android:orientation="vertical"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintRight_toLeftOf="@id/guideline"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_max="wrap"
app:layout_constraintWidth_percent="0.5"
app:singleSelection="true"
android:layout_marginStart="16dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/current_date_selection"
style="@style/DateTimeShortcuts"
tools:text="Nov 24, 2021"
android:visibility="gone"
app:icon="@drawable/ic_outline_today_24px"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/due_date_button"
style="@style/DateTimeShortcuts"
android:text="@string/due_date"
app:icon="@drawable/ic_calendar_today_24px"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/due_time_button"
style="@style/DateTimeShortcuts"
app:icon="@drawable/ic_outline_schedule_24px"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:text="@string/due_time" />
<com.google.android.material.button.MaterialButton
android:id="@+id/day_before_due_button"
style="@style/DateTimeShortcuts"
android:text="@string/day_before_due"
app:icon="@drawable/ic_outline_wb_sunny_24px"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/due_date_button" />
<com.google.android.material.button.MaterialButton
android:id="@+id/week_before_due_button"
style="@style/DateTimeShortcuts"
app:icon="@drawable/ic_date_range_24px"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/day_before_due_button"
android:text="@string/week_before_due" />
<com.google.android.material.button.MaterialButton
android:id="@+id/no_date_button"
style="@style/DateTimeShortcuts"
android:text="@string/no_date"
app:icon="@drawable/ic_outline_not_interested_24px"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/week_before_due_button" />
</com.google.android.material.button.MaterialButtonToggleGroup>
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
<com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/time_group"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/half_keyline_first"
android:layout_marginBottom="@dimen/half_keyline_first"
android:orientation="vertical"
app:layout_constraintLeft_toRightOf="@id/guideline"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintWidth_max="wrap"
app:layout_constraintWidth_percent="0.5"
app:selectionRequired="true"
app:singleSelection="true"
android:layout_marginEnd="16dp">
<com.google.android.material.button.MaterialButton
android:id="@+id/current_time_selection"
style="@style/DateTimeShortcuts"
app:icon="@drawable/ic_outline_schedule_24px"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:visibility="visible"
tools:text="08:15" />
<com.google.android.material.button.MaterialButton
android:id="@+id/morning_button"
style="@style/DateTimeShortcuts"
tools:text="9 AM"
app:icon="@drawable/ic_local_cafe_24px"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/afternoon_button"
style="@style/DateTimeShortcuts"
android:layout_marginTop="1dp"
tools:text="1 PM"
app:icon="@drawable/ic_outline_wb_sunny_24px"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/evening_button"
style="@style/DateTimeShortcuts"
tools:text="5 PM"
app:icon="@drawable/ic_weather_sunset"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/night_button"
style="@style/DateTimeShortcuts"
tools:text="8 PM"
app:icon="@drawable/ic_nights_stay_24px"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/pick_time_button"
style="@style/DateTimeShortcuts"
android:text="@string/shortcut_pick_time"
app:icon="@drawable/ic_outline_schedule_24px"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.button.MaterialButton
android:id="@+id/no_time"
style="@style/DateTimeShortcuts"
android:text="@string/no_time"
app:icon="@drawable/ic_outline_not_interested_24px"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</com.google.android.material.button.MaterialButtonToggleGroup>
</androidx.constraintlayout.widget.ConstraintLayout>

@ -98,20 +98,6 @@
<item name="android:textColor">@color/text_primary</item> <item name="android:textColor">@color/text_primary</item>
</style> </style>
<style name="DateTimeShortcuts" parent="Widget.Material3.Button.TextButton">
<item name="android:textColor">@color/button_accent_text</item>
<item name="android:textAppearance">@style/TextAppearance.Material3.BodyMedium</item>
<item name="android:paddingTop">@dimen/quarter_keyline_first</item>
<item name="android:paddingBottom">@dimen/quarter_keyline_first</item>
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="iconPadding">@dimen/keyline_first</item>
<item name="iconTint">@color/button_accent_text</item>
<item name="android:gravity">start|center_vertical</item>
<item name="backgroundTint">@android:color/transparent</item>
<item name="rippleColor">?attr/colorSecondary</item>
</style>
<!--=============================================== MainActivity == --> <!--=============================================== MainActivity == -->
<style name="BaseHorizontalDivider"> <style name="BaseHorizontalDivider">

Loading…
Cancel
Save