From 33a7a0a53f096f3199b0d51bdec46aa26ee88241 Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Sat, 15 Mar 2025 12:05:15 -0500 Subject: [PATCH] Compose date time pickers --- .../compose/pickers/DatePickerShortcuts.kt | 350 +++++++++++++++++ .../tasks/compose/pickers/TimePickerDialog.kt | 139 +++++++ .../org/tasks/dialogs/BaseDateTimePicker.kt | 121 +----- .../java/org/tasks/dialogs/DateTimePicker.kt | 364 +++++++++--------- .../java/org/tasks/dialogs/PriorityPicker.kt | 2 - .../java/org/tasks/dialogs/StartDatePicker.kt | 261 +++++++------ app/src/main/res/color/button_accent_text.xml | 7 - app/src/main/res/color/clock_text_color.xml | 20 - .../res/layout/date_time_picker_shortcuts.xml | 148 ------- .../res/layout/dialog_date_time_picker.xml | 29 -- .../dialog_date_time_picker_buttons.xml | 43 --- .../res/layout/dialog_start_date_picker.xml | 28 -- .../layout/start_date_picker_shortcuts.xml | 149 ------- app/src/main/res/values/styles.xml | 14 - 14 files changed, 795 insertions(+), 880 deletions(-) create mode 100644 app/src/main/java/org/tasks/compose/pickers/DatePickerShortcuts.kt create mode 100644 app/src/main/java/org/tasks/compose/pickers/TimePickerDialog.kt delete mode 100644 app/src/main/res/color/button_accent_text.xml delete mode 100644 app/src/main/res/color/clock_text_color.xml delete mode 100644 app/src/main/res/layout/date_time_picker_shortcuts.xml delete mode 100644 app/src/main/res/layout/dialog_date_time_picker.xml delete mode 100644 app/src/main/res/layout/dialog_date_time_picker_buttons.xml delete mode 100644 app/src/main/res/layout/dialog_start_date_picker.xml delete mode 100644 app/src/main/res/layout/start_date_picker_shortcuts.xml diff --git a/app/src/main/java/org/tasks/compose/pickers/DatePickerShortcuts.kt b/app/src/main/java/org/tasks/compose/pickers/DatePickerShortcuts.kt new file mode 100644 index 000000000..458a38ea8 --- /dev/null +++ b/app/src/main/java/org/tasks/compose/pickers/DatePickerShortcuts.kt @@ -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, + ) + } + } +} diff --git a/app/src/main/java/org/tasks/compose/pickers/TimePickerDialog.kt b/app/src/main/java/org/tasks/compose/pickers/TimePickerDialog.kt new file mode 100644 index 000000000..166cee1d5 --- /dev/null +++ b/app/src/main/java/org/tasks/compose/pickers/TimePickerDialog.kt @@ -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)) + } + } + } + } + } + } +} diff --git a/app/src/main/java/org/tasks/dialogs/BaseDateTimePicker.kt b/app/src/main/java/org/tasks/dialogs/BaseDateTimePicker.kt index 9bb36c1d3..babc287cc 100644 --- a/app/src/main/java/org/tasks/dialogs/BaseDateTimePicker.kt +++ b/app/src/main/java/org/tasks/dialogs/BaseDateTimePicker.kt @@ -1,47 +1,21 @@ package org.tasks.dialogs import android.app.Activity -import android.app.Dialog import android.content.DialogInterface -import android.os.Bundle -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 androidx.fragment.app.DialogFragment import org.tasks.preferences.Preferences import org.tasks.themes.Theme -import org.tasks.time.DateTimeUtils2.currentTimeMillis -import org.tasks.time.withMillisOfDay import javax.inject.Inject -abstract class BaseDateTimePicker : BottomSheetDialogFragment() { +abstract class BaseDateTimePicker : DialogFragment() { @Inject lateinit var theme: Theme @Inject lateinit var preferences: Preferences - protected var morning = 32401000 - protected var afternoon = 46801000 - protected var evening = 61201000 - protected var night = 72001000 - interface OnDismissHandler { fun onDismiss() } - private var onDismissHandler: OnDismissHandler? = null + protected var onDismissHandler: OnDismissHandler? = null override fun onAttach(activity: Activity) { super.onAttach(activity) @@ -57,100 +31,13 @@ abstract class BaseDateTimePicker : BottomSheetDialogFragment() { onDismissHandler?.onDismiss() } - override fun onResume() { - super.onResume() - - refreshButtons() - } - override fun onCancel(dialog: DialogInterface) = sendSelected() - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val dialog = super.onCreateDialog(savedInstanceState) as BottomSheetDialog - dialog.setOnShowListener { - dialog - .findViewById(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(com.google.android.material.R.id.coordinator) - val containerLayout = - dialog.findViewById(com.google.android.material.R.id.container) - val buttons = theme.getLayoutInflater(requireContext()) - .inflate(R.layout.dialog_date_time_picker_buttons, null) - buttons.findViewById(R.id.cancel_button).setOnClickListener { dismiss() } - buttons.findViewById(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 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 refreshButtons() - companion object { const val EXTRA_AUTO_CLOSE = "extra_auto_close" } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/tasks/dialogs/DateTimePicker.kt b/app/src/main/java/org/tasks/dialogs/DateTimePicker.kt index a6dbf3b3d..203b6b44a 100644 --- a/app/src/main/java/org/tasks/dialogs/DateTimePicker.kt +++ b/app/src/main/java/org/tasks/dialogs/DateTimePicker.kt @@ -5,38 +5,48 @@ import android.app.Activity.RESULT_OK import android.content.Intent import android.os.Bundle import android.view.LayoutInflater -import android.view.View 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.compose.content import androidx.lifecycle.lifecycleScope import com.todoroo.astrid.dao.TaskDao import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import org.tasks.R +import org.tasks.compose.pickers.DatePickerShortcuts +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.entity.Task -import org.tasks.databinding.DialogDateTimePickerBinding import org.tasks.date.DateTimeUtils.newDateTime 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.kmp.org.tasks.time.getRelativeDay -import org.tasks.kmp.org.tasks.time.getTimeString import org.tasks.notifications.NotificationManager +import org.tasks.themes.TasksTheme import org.tasks.time.DateTime import org.tasks.time.millisOfDay 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 @AndroidEntryPoint @@ -46,19 +56,9 @@ class DateTimePicker : BaseDateTimePicker() { @Inject lateinit var taskDao: TaskDao @Inject lateinit var notificationManager: NotificationManager - lateinit var binding: DialogDateTimePickerBinding - private var customDate = NO_DAY - private var customTime = NO_TIME - private var selectedDay = NO_DAY - private var selectedTime = NO_TIME + private var selectedDay by mutableLongStateOf(NO_DAY) + private var selectedTime by mutableIntStateOf(NO_TIME) 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 { const val EXTRA_DAY = "extra_day" @@ -66,16 +66,14 @@ class DateTimePicker : BaseDateTimePicker() { const val EXTRA_TASKS = "extra_tasks" const val EXTRA_TIMESTAMP = "extra_timestamp" const val EXTRA_HIDE_NO_DATE = "extra_hide_no_date" - private const val REQUEST_TIME = 10101 - private const val FRAG_TAG_TIME_PICKER = "frag_tag_time_picker" - private const val NO_DAY = 0L - private const val NO_TIME = 0 - private const val MULTIPLE_DAYS = -1L - private const val MULTIPLE_TIMES = -1 + const val NO_DAY = 0L + const val NO_TIME = 0 + const val MULTIPLE_DAYS = -1L + const val MULTIPLE_TIMES = -1 fun newDateTimePicker( - autoClose: Boolean, - vararg tasks: Task + autoClose: Boolean, + vararg tasks: Task, ): DateTimePicker { val fragment = DateTimePicker() val dueDates = tasks.map { it.dueDate.startOfDay() }.toSet() @@ -91,11 +89,11 @@ class DateTimePicker : BaseDateTimePicker() { } fun newDateTimePicker( - target: Fragment, - rc: Int, - current: Long, - autoClose: Boolean, - hideNoDate: Boolean, + target: Fragment, + rc: Int, + current: Long, + autoClose: Boolean, + hideNoDate: Boolean, ): DateTimePicker { val fragment = DateTimePicker() fragment.arguments = Bundle().apply { @@ -109,126 +107,117 @@ class DateTimePicker : BaseDateTimePicker() { } } - override fun onCreateView( - inflater: LayoutInflater, - 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 + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) - 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() { - when (selectedDay) { - 0L -> binding.shortcuts.dateGroup.check(R.id.no_date_button) - today.millis -> binding.shortcuts.dateGroup.check(R.id.today_button) - tomorrow.millis -> binding.shortcuts.dateGroup.check(R.id.tomorrow_button) - nextWeek.millis -> binding.shortcuts.dateGroup.check(R.id.next_week_button) - else -> { - customDate = selectedDay - binding.shortcuts.dateGroup.check(R.id.current_date_selection) - binding.shortcuts.currentDateSelection.visibility = View.VISIBLE - binding.shortcuts.currentDateSelection.text = if (customDate == MULTIPLE_DAYS) { - requireContext().getString(R.string.date_picker_multiple) - } else { - runBlocking { - getRelativeDay(selectedDay) + @OptIn(ExperimentalMaterial3Api::class) + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ) = content { + TasksTheme(theme = theme.themeBase.index) { + val sheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true, + ) + ModalBottomSheet( + sheetState = sheetState, + onDismissRequest = { + onDismissHandler?.onDismiss() ?: dismiss() + }, + 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 } } - } - } - if (selectedTime == MULTIPLE_TIMES || Task.hasDueTime(selectedTime.toLong())) { - when (selectedTime) { - morning -> binding.shortcuts.timeGroup.check(R.id.morning_button) - afternoon -> binding.shortcuts.timeGroup.check(R.id.afternoon_button) - 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 - ) - } + LaunchedEffect(state.selectedDateMillis) { + if (state.selectedDateMillis == selectedDay + (DateTime(selectedDay).offset)) { + return@LaunchedEffect + } + state.selectedDateMillis?.let { + returnDate(day = it - DateTime(it).offset) + } } } - } 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) { val day = when { selectedDay == MULTIPLE_DAYS -> MULTIPLE_DAYS @@ -244,8 +233,6 @@ class DateTimePicker : BaseDateTimePicker() { selectedTime = time if (closeAutomatically()) { sendSelected() - } else { - refreshButtons() } } @@ -254,43 +241,49 @@ class DateTimePicker : BaseDateTimePicker() { override fun sendSelected() { if (selectedDay != arguments?.getLong(EXTRA_DAY) - || selectedTime != arguments?.getInt(EXTRA_TIME)) { + || selectedTime != arguments?.getInt(EXTRA_TIME) + ) { if (taskIds.isEmpty()) { val intent = Intent() - intent.putExtra(EXTRA_TIMESTAMP, when { - selectedDay == NO_DAY -> 0 - selectedTime == NO_TIME -> selectedDay - else -> selectedDay.toDateTime().withMillisOfDay(selectedTime).millis - }) + intent.putExtra( + EXTRA_TIMESTAMP, when { + selectedDay == NO_DAY -> 0 + selectedTime == NO_TIME -> selectedDay + else -> selectedDay.toDateTime().withMillisOfDay(selectedTime).millis + } + ) targetFragment?.onActivityResult(targetRequestCode, RESULT_OK, intent) } else { lifecycleScope.launch(NonCancellable) { taskDao - .fetch(taskIds.toList()) - .forEach { - val day = if (selectedDay == MULTIPLE_DAYS) { - if (it.hasDueDate()) it.dueDate else today.millis - } else { - selectedDay - } - val time = if (selectedTime == MULTIPLE_TIMES) { - if (it.hasDueTime()) it.dueDate.millisOfDay else NO_TIME - } else { - selectedTime - } - it.setDueDateAdjustingHideUntil(when { + .fetch(taskIds.toList()) + .forEach { + val day = if (selectedDay == MULTIPLE_DAYS) { + if (it.hasDueDate()) it.dueDate else today.millis + } else { + selectedDay + } + val time = if (selectedTime == MULTIPLE_TIMES) { + if (it.hasDueTime()) it.dueDate.millisOfDay else NO_TIME + } else { + selectedTime + } + it.setDueDateAdjustingHideUntil( + when { day == NO_DAY -> 0L time == NO_TIME -> createDueDate( - Task.URGENCY_SPECIFIC_DAY, - day + Task.URGENCY_SPECIFIC_DAY, + day ) + else -> createDueDate( - Task.URGENCY_SPECIFIC_DAY_TIME, - day.toDateTime().withMillisOfDay(time).millis + Task.URGENCY_SPECIFIC_DAY_TIME, + day.toDateTime().withMillisOfDay(time).millis ) - }) - taskDao.save(it) - } + } + ) + taskDao.save(it) + } } } } @@ -303,17 +296,4 @@ class DateTimePicker : BaseDateTimePicker() { outState.putLong(EXTRA_DAY, selectedDay) 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) - } - } } diff --git a/app/src/main/java/org/tasks/dialogs/PriorityPicker.kt b/app/src/main/java/org/tasks/dialogs/PriorityPicker.kt index cc2d67ac9..0f57d49e4 100644 --- a/app/src/main/java/org/tasks/dialogs/PriorityPicker.kt +++ b/app/src/main/java/org/tasks/dialogs/PriorityPicker.kt @@ -15,7 +15,6 @@ import kotlinx.coroutines.launch import org.tasks.R import org.tasks.compose.edit.Priority import org.tasks.data.entity.Task -import org.tasks.databinding.DialogDateTimePickerBinding import org.tasks.databinding.DialogPriorityPickerBinding import javax.inject.Inject @@ -39,7 +38,6 @@ class PriorityPicker : DialogFragment() { } } - lateinit var binding: DialogDateTimePickerBinding private val priorityPickerViewModel: PriorityPickerViewModel by viewModels() override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { diff --git a/app/src/main/java/org/tasks/dialogs/StartDatePicker.kt b/app/src/main/java/org/tasks/dialogs/StartDatePicker.kt index 2a215faae..a6c74704e 100644 --- a/app/src/main/java/org/tasks/dialogs/StartDatePicker.kt +++ b/app/src/main/java/org/tasks/dialogs/StartDatePicker.kt @@ -5,23 +5,41 @@ import android.app.Activity.RESULT_OK import android.content.Intent import android.os.Bundle import android.view.LayoutInflater -import android.view.View 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.compose.content import com.todoroo.astrid.dao.TaskDao import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.runBlocking -import org.tasks.R +import org.tasks.compose.pickers.DatePickerShortcuts +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.databinding.DialogStartDatePickerBinding 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.kmp.org.tasks.time.getRelativeDay -import org.tasks.kmp.org.tasks.time.getTimeString import org.tasks.notifications.NotificationManager +import org.tasks.themes.TasksTheme import org.tasks.time.DateTime -import org.tasks.time.withMillisOfDay import javax.inject.Inject @AndroidEntryPoint @@ -31,21 +49,11 @@ class StartDatePicker : BaseDateTimePicker() { @Inject lateinit var taskDao: TaskDao @Inject lateinit var notificationManager: NotificationManager - lateinit var binding: DialogStartDatePickerBinding - private var customDate = NO_DAY - private var customTime = NO_TIME - private var selectedDay = NO_DAY - private var selectedTime = NO_TIME + private var selectedDay by mutableLongStateOf(NO_DAY) + private var selectedTime by mutableIntStateOf(NO_TIME) 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 { - 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_TIME = "extra_time" const val NO_DAY = 0L @@ -67,110 +75,116 @@ class StartDatePicker : BaseDateTimePicker() { } } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - binding = DialogStartDatePickerBinding.inflate(theme.getLayoutInflater(requireContext())) - setupShortcutsAndCalendar() - binding.calendarView.setOnDateChangeListener { _, y, m, d -> - returnDate(day = DateTime(y, m + 1, d).millis) - refreshButtons() - } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + selectedDay = savedInstanceState?.getLong(EXTRA_DAY) ?: requireArguments().getLong(EXTRA_DAY) selectedTime = - savedInstanceState?.getInt(EXTRA_TIME) - ?: requireArguments().getInt(EXTRA_TIME) - .takeIf { Task.hasDueTime(it.toLong()) } - ?: 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 + savedInstanceState?.getInt(EXTRA_TIME) + ?: requireArguments().getInt(EXTRA_TIME) + .takeIf { Task.hasDueTime(it.toLong()) } + ?: NO_TIME } - override fun refreshButtons() { - when (selectedDay) { - 0L -> binding.shortcuts.dateGroup.check(R.id.no_date_button) - DUE_DATE -> binding.shortcuts.dateGroup.check(R.id.due_date_button) - DUE_TIME -> { - binding.shortcuts.dateGroup.check(R.id.due_time_button) - binding.shortcuts.timeGroup.clearChecked() - } - DAY_BEFORE_DUE -> binding.shortcuts.dateGroup.check(R.id.day_before_due_button) - WEEK_BEFORE_DUE -> binding.shortcuts.dateGroup.check(R.id.week_before_due_button) - else -> { - customDate = selectedDay - binding.shortcuts.dateGroup.check(R.id.current_date_selection) - binding.shortcuts.currentDateSelection.visibility = View.VISIBLE - binding.shortcuts.currentDateSelection.text = - runBlocking { - getRelativeDay(selectedDay) - } - } - } - if (Task.hasDueTime(selectedTime.toLong())) { - when (selectedTime) { - morning -> binding.shortcuts.timeGroup.check(R.id.morning_button) - afternoon -> binding.shortcuts.timeGroup.check(R.id.afternoon_button) - 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 = getTimeString( - today.millis.withMillisOfDay(selectedTime), - requireContext().is24HourFormat + @OptIn(ExperimentalMaterial3Api::class) + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = content { + TasksTheme(theme = theme.themeBase.index) { + val sheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true, + ) + ModalBottomSheet( + sheetState = sheetState, + onDismissRequest = { + onDismissHandler?.onDismiss() ?: dismiss() + }, + 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 < 0 || !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 = 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) { @@ -188,8 +202,6 @@ class StartDatePicker : BaseDateTimePicker() { selectedTime = time if (closeAutomatically()) { sendSelected() - } else { - refreshButtons() } } @@ -211,17 +223,4 @@ class StartDatePicker : BaseDateTimePicker() { outState.putLong(EXTRA_DAY, selectedDay) 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) - } - } } diff --git a/app/src/main/res/color/button_accent_text.xml b/app/src/main/res/color/button_accent_text.xml deleted file mode 100644 index 08b2a61bb..000000000 --- a/app/src/main/res/color/button_accent_text.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/color/clock_text_color.xml b/app/src/main/res/color/clock_text_color.xml deleted file mode 100644 index 3e706f105..000000000 --- a/app/src/main/res/color/clock_text_color.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - diff --git a/app/src/main/res/layout/date_time_picker_shortcuts.xml b/app/src/main/res/layout/date_time_picker_shortcuts.xml deleted file mode 100644 index d1eb2f9bf..000000000 --- a/app/src/main/res/layout/date_time_picker_shortcuts.xml +++ /dev/null @@ -1,148 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_date_time_picker.xml b/app/src/main/res/layout/dialog_date_time_picker.xml deleted file mode 100644 index d4550195b..000000000 --- a/app/src/main/res/layout/dialog_date_time_picker.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/layout/dialog_date_time_picker_buttons.xml b/app/src/main/res/layout/dialog_date_time_picker_buttons.xml deleted file mode 100644 index 51d70de42..000000000 --- a/app/src/main/res/layout/dialog_date_time_picker_buttons.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/dialog_start_date_picker.xml b/app/src/main/res/layout/dialog_start_date_picker.xml deleted file mode 100644 index 98f421eb1..000000000 --- a/app/src/main/res/layout/dialog_start_date_picker.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/layout/start_date_picker_shortcuts.xml b/app/src/main/res/layout/start_date_picker_shortcuts.xml deleted file mode 100644 index f41e28a0c..000000000 --- a/app/src/main/res/layout/start_date_picker_shortcuts.xml +++ /dev/null @@ -1,149 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 30e0ed8a7..c29c7e968 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -98,20 +98,6 @@ @color/text_primary - -