From 41f2f51c3747f2a5e6346d6ab63d1303546f51b6 Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Sun, 23 Mar 2025 12:27:04 -0500 Subject: [PATCH] Add DueDatePicker preview and fix layout issues --- .../activities/DateAndTimePickerActivity.kt | 8 +- .../compose/pickers/DatePickerBottomSheet.kt | 6 +- .../compose/pickers/DatePickerShortcuts.kt | 6 + .../tasks/compose/pickers/TimePickerDialog.kt | 10 +- .../java/org/tasks/dialogs/DateTimePicker.kt | 250 +++++++++++++----- .../org/tasks/dialogs/MyTimePickerDialog.kt | 8 +- .../java/org/tasks/dialogs/StartDatePicker.kt | 12 +- 7 files changed, 219 insertions(+), 81 deletions(-) diff --git a/app/src/main/java/org/tasks/activities/DateAndTimePickerActivity.kt b/app/src/main/java/org/tasks/activities/DateAndTimePickerActivity.kt index d9bb1b26d..ee6c950d7 100644 --- a/app/src/main/java/org/tasks/activities/DateAndTimePickerActivity.kt +++ b/app/src/main/java/org/tasks/activities/DateAndTimePickerActivity.kt @@ -5,6 +5,7 @@ import android.os.Bundle import androidx.activity.compose.setContent import androidx.appcompat.app.AppCompatActivity import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.rememberTimePickerState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf @@ -41,8 +42,11 @@ class DateAndTimePickerActivity : AppCompatActivity() { var showTimePicker by rememberSaveable { mutableStateOf(false) } if (showTimePicker) { TimePickerDialog( - millisOfDay = 0, - is24Hour = is24HourFormat, + state = rememberTimePickerState( + initialHour = 0, + initialMinute = 0, + is24Hour = is24HourFormat + ), initialDisplayMode = remember { preferences.timeDisplayMode }, setDisplayMode = { preferences.timeDisplayMode = it }, selected = { diff --git a/app/src/main/java/org/tasks/compose/pickers/DatePickerBottomSheet.kt b/app/src/main/java/org/tasks/compose/pickers/DatePickerBottomSheet.kt index 73edf04a1..364da7f02 100644 --- a/app/src/main/java/org/tasks/compose/pickers/DatePickerBottomSheet.kt +++ b/app/src/main/java/org/tasks/compose/pickers/DatePickerBottomSheet.kt @@ -25,6 +25,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.SheetState import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -39,6 +40,7 @@ import org.tasks.R @OptIn(ExperimentalMaterial3Api::class) @Composable fun DatePickerBottomSheet( + sheetState: SheetState, showButtons: Boolean, dismiss: () -> Unit, accept: () -> Unit, @@ -49,9 +51,7 @@ fun DatePickerBottomSheet( ) { ModalBottomSheet( modifier = Modifier.statusBarsPadding(), - sheetState = rememberModalBottomSheetState( - skipPartiallyExpanded = true, - ), + sheetState = sheetState, onDismissRequest = { dismiss() }, containerColor = MaterialTheme.colorScheme.surface, ) { diff --git a/app/src/main/java/org/tasks/compose/pickers/DatePickerShortcuts.kt b/app/src/main/java/org/tasks/compose/pickers/DatePickerShortcuts.kt index 458a38ea8..c933b0506 100644 --- a/app/src/main/java/org/tasks/compose/pickers/DatePickerShortcuts.kt +++ b/app/src/main/java/org/tasks/compose/pickers/DatePickerShortcuts.kt @@ -8,6 +8,7 @@ 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.layout.widthIn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.outlined.NextWeek import androidx.compose.material.icons.outlined.AccessTime @@ -34,8 +35,10 @@ 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.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import kotlinx.coroutines.runBlocking import org.tasks.R @@ -77,6 +80,7 @@ fun DatePickerShortcuts( ) { Column( horizontalAlignment = Alignment.Start, + modifier = Modifier.widthIn(max = LocalConfiguration.current.screenWidthDp.dp / 2) ) { dateShortcuts() } @@ -344,6 +348,8 @@ fun ShortcutButton( ) Text( text = text, + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) } } diff --git a/app/src/main/java/org/tasks/compose/pickers/TimePickerDialog.kt b/app/src/main/java/org/tasks/compose/pickers/TimePickerDialog.kt index c40f379f6..66a86e8dd 100644 --- a/app/src/main/java/org/tasks/compose/pickers/TimePickerDialog.kt +++ b/app/src/main/java/org/tasks/compose/pickers/TimePickerDialog.kt @@ -24,7 +24,7 @@ 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.material3.TimePickerState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -41,18 +41,12 @@ import org.tasks.R @OptIn(ExperimentalMaterial3Api::class) @Composable fun TimePickerDialog( - millisOfDay: Int, - is24Hour: Boolean, + state: TimePickerState, initialDisplayMode: DisplayMode, setDisplayMode: (DisplayMode) -> Unit, selected: (Int) -> Unit, dismiss: () -> Unit, ) { - val state = rememberTimePickerState( - initialHour = millisOfDay / (60 * 60_000), - initialMinute = (millisOfDay / (60_000)) % 60, - is24Hour = is24Hour - ) var displayMode by remember { mutableStateOf(initialDisplayMode) } val layoutType = with(LocalConfiguration.current) { if (screenHeightDp < screenWidthDp) { diff --git a/app/src/main/java/org/tasks/dialogs/DateTimePicker.kt b/app/src/main/java/org/tasks/dialogs/DateTimePicker.kt index 56ad13115..585e5c068 100644 --- a/app/src/main/java/org/tasks/dialogs/DateTimePicker.kt +++ b/app/src/main/java/org/tasks/dialogs/DateTimePicker.kt @@ -6,8 +6,14 @@ import android.content.Intent import android.os.Bundle import android.view.LayoutInflater import android.view.ViewGroup +import androidx.compose.material3.DatePickerState +import androidx.compose.material3.DisplayMode import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SheetState import androidx.compose.material3.rememberDatePickerState +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.rememberTimePickerState +import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -16,6 +22,10 @@ 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.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewFontScale +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.tooling.preview.PreviewScreenSizes import androidx.fragment.app.Fragment import androidx.fragment.compose.content import androidx.lifecycle.lifecycleScope @@ -23,6 +33,7 @@ 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.compose.pickers.DatePickerBottomSheet import org.tasks.compose.pickers.DueDateShortcuts import org.tasks.compose.pickers.TimePickerDialog @@ -31,12 +42,19 @@ import org.tasks.data.createDueDate import org.tasks.data.entity.Task import org.tasks.date.DateTimeUtils.newDateTime import org.tasks.date.DateTimeUtils.toDateTime +import org.tasks.dialogs.DateTimePicker.Companion.MULTIPLE_TIMES +import org.tasks.dialogs.DateTimePicker.Companion.NO_TIME import org.tasks.extensions.Context.is24HourFormat import org.tasks.notifications.NotificationManager import org.tasks.themes.TasksTheme import org.tasks.time.DateTime +import org.tasks.time.DateTimeUtils2.currentTimeMillis import org.tasks.time.millisOfDay +import org.tasks.time.noon +import org.tasks.time.plusDays import org.tasks.time.startOfDay +import org.tasks.time.withMillisOfDay +import java.util.concurrent.TimeUnit import javax.inject.Inject @AndroidEntryPoint @@ -116,83 +134,49 @@ class DateTimePicker : BaseDateTimePicker() { savedInstanceState: Bundle?, ) = content { TasksTheme(theme = theme.themeBase.index) { - val state = rememberDatePickerState( + val datePickerState = rememberDatePickerState( initialDisplayMode = remember { preferences.calendarDisplayMode }, ) - DatePickerBottomSheet( - state = state, + DueDatePicker( + sheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true + ), + datePickerState = datePickerState, + initialTimeDisplayMode = remember { preferences.timeDisplayMode }, + selectedDay = selectedDay, + selectedTime = selectedTime, showButtons = !autoclose, - setDisplayMode = { preferences.calendarDisplayMode = it }, - dismiss = { onDismissHandler?.onDismiss() ?: dismiss() }, - accept = { sendSelected() }, - 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) }, + showNoDate = remember { + !requireArguments().getBoolean( + EXTRA_HIDE_NO_DATE, + false ) }, - 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 }, - initialDisplayMode = remember { preferences.timeDisplayMode }, - setDisplayMode = { preferences.timeDisplayMode = it }, - 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) }, - ) - } + setDateDisplayMode = { preferences.calendarDisplayMode = it }, + setTimeDisplayMode = { preferences.timeDisplayMode = it }, + dismiss = { onDismissHandler?.onDismiss() ?: dismiss() }, + accept = { sendSelected() }, + setDateTime = { day, time -> returnDate(day, time) }, + setTime = { returnSelectedTime(it) }, + is24Hour = remember { requireContext().is24HourFormat }, + today = today.millis, + morning = remember { preferences.dateShortcutMorning + 1000 }, + afternoon = remember { preferences.dateShortcutAfternoon + 1000 }, + evening = remember { preferences.dateShortcutEvening + 1000 }, + night = remember { preferences.dateShortcutNight + 1000 }, ) LaunchedEffect(selectedDay) { if (selectedDay > 0) { - state.selectedDateMillis = selectedDay + (DateTime(selectedDay).offset) + datePickerState.selectedDateMillis = selectedDay + (DateTime(selectedDay).offset) } else { - state.selectedDateMillis = null + datePickerState.selectedDateMillis = null } } - LaunchedEffect(state.selectedDateMillis) { - if (state.selectedDateMillis == selectedDay + (DateTime(selectedDay).offset)) { + LaunchedEffect(datePickerState.selectedDateMillis) { + if (datePickerState.selectedDateMillis == selectedDay + (DateTime(selectedDay).offset)) { return@LaunchedEffect } - state.selectedDateMillis?.let { + datePickerState.selectedDateMillis?.let { returnDate(day = it - DateTime(it).offset) } } @@ -201,6 +185,7 @@ class DateTimePicker : BaseDateTimePicker() { private fun returnSelectedTime(millisOfDay: Int) { val day = when { + millisOfDay == NO_TIME -> selectedDay selectedDay == MULTIPLE_DAYS -> MULTIPLE_DAYS selectedDay > 0 -> selectedDay today.withMillisOfDay(millisOfDay).isAfterNow -> today.millis @@ -278,3 +263,140 @@ class DateTimePicker : BaseDateTimePicker() { outState.putInt(EXTRA_TIME, selectedTime) } } + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DueDatePicker( + sheetState: SheetState, + datePickerState: DatePickerState, + initialTimeDisplayMode: DisplayMode, + selectedDay: Long, + selectedTime: Int, + today: Long, + morning: Int, + afternoon: Int, + evening: Int, + night: Int, + is24Hour: Boolean, + showButtons: Boolean, + showNoDate: Boolean, + setDateDisplayMode: (DisplayMode) -> Unit, + setTimeDisplayMode: (DisplayMode) -> Unit, + dismiss: () -> Unit, + accept: () -> Unit, + setDateTime: (Long, Int) -> Unit, + setTime: (Int) -> Unit, +) { + DatePickerBottomSheet( + sheetState = sheetState, + state = datePickerState, + showButtons = showButtons, + setDisplayMode = setDateDisplayMode, + dismiss = dismiss, + accept = accept, + dateShortcuts = { + DueDateShortcuts( + today = today, + tomorrow = remember { today.plusDays(1) }, + nextWeek = remember { today.plusDays(7) }, + selected = selectedDay, + showNoDate = showNoDate, + selectedDay = { setDateTime(it.startOfDay(), selectedTime) }, + clearDate = { setDateTime(0, 0) }, + ) + }, + timeShortcuts = { + var showTimePicker by rememberSaveable { + mutableStateOf( + false + ) + } + if (showTimePicker) { + val time = if (selectedTime == MULTIPLE_TIMES + || !Task.hasDueTime( + today.withMillisOfDay( + selectedTime + ) + ) + ) { + today.noon().millisOfDay + } else { + selectedTime + } + TimePickerDialog( + state = rememberTimePickerState( + initialHour = time / (60 * 60_000), + initialMinute = (time / (60_000)) % 60, + is24Hour = is24Hour, + ), + initialDisplayMode = initialTimeDisplayMode, + setDisplayMode = setTimeDisplayMode, + selected = { setTime(it + 1000) }, + dismiss = { showTimePicker = false }, + ) + } + TimeShortcuts( + day = 0, + selected = selectedTime, + morning = morning, + afternoon = afternoon, + evening = evening, + night = night, + selectedMillisOfDay = { setTime(it) }, + pickTime = { showTimePicker = true }, + clearTime = { setTime(NO_TIME) }, + ) + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@PreviewLightDark +@PreviewFontScale +@PreviewScreenSizes +@Preview( + locale = "es", + fontScale = 2f +) +@Preview( + locale = "es", +) +@Preview( + locale = "de", + fontScale = 2f +) +@Preview( + locale = "de", +) +@Composable +fun DueDatePickerPreview() { + TasksTheme { + val today = currentTimeMillis().startOfDay() + val sheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true + ).apply { + runBlocking { show() } + } + DueDatePicker( + sheetState = sheetState, + datePickerState = rememberDatePickerState(), + initialTimeDisplayMode = DisplayMode.Input, + selectedDay = 0, + selectedTime = 0, + today = today, + morning = TimeUnit.HOURS.toMillis(9).toInt(), + afternoon = TimeUnit.HOURS.toMillis(13).toInt(), + evening = TimeUnit.HOURS.toMillis(17).toInt(), + night = TimeUnit.HOURS.toMillis(20).toInt(), + is24Hour = true, + showButtons = true, + showNoDate = true, + setDateDisplayMode = {}, + setTimeDisplayMode = {}, + dismiss = {}, + accept = {}, + setDateTime = { _, _ -> }, + setTime = {}, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/tasks/dialogs/MyTimePickerDialog.kt b/app/src/main/java/org/tasks/dialogs/MyTimePickerDialog.kt index 715b52b1d..f50a95166 100644 --- a/app/src/main/java/org/tasks/dialogs/MyTimePickerDialog.kt +++ b/app/src/main/java/org/tasks/dialogs/MyTimePickerDialog.kt @@ -6,6 +6,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.ViewGroup import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.rememberTimePickerState import androidx.compose.runtime.remember import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment @@ -39,8 +40,11 @@ class MyTimePickerDialog : DialogFragment() { primary = theme.themeColor.primaryColor, ) { TimePickerDialog( - millisOfDay = remember { initial.millisOfDay }, - is24Hour = remember { requireContext().is24HourFormat }, + state = rememberTimePickerState( + initialHour = initial.millisOfDay / (60 * 60_000), + initialMinute = (initial.millisOfDay / (60_000)) % 60, + is24Hour = requireContext().is24HourFormat + ), initialDisplayMode = remember { preferences.timeDisplayMode }, setDisplayMode = { preferences.timeDisplayMode = it }, selected = { diff --git a/app/src/main/java/org/tasks/dialogs/StartDatePicker.kt b/app/src/main/java/org/tasks/dialogs/StartDatePicker.kt index 31cbfd8f7..7729b2671 100644 --- a/app/src/main/java/org/tasks/dialogs/StartDatePicker.kt +++ b/app/src/main/java/org/tasks/dialogs/StartDatePicker.kt @@ -8,6 +8,8 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.rememberDatePickerState +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.rememberTimePickerState import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf @@ -87,6 +89,9 @@ class StartDatePicker : BaseDateTimePicker() { initialDisplayMode = remember { preferences.calendarDisplayMode }, ) DatePickerBottomSheet( + sheetState = rememberModalBottomSheetState( + skipPartiallyExpanded = true + ), state = state, showButtons = !autoclose, setDisplayMode = { preferences.calendarDisplayMode = it }, @@ -112,8 +117,11 @@ class StartDatePicker : BaseDateTimePicker() { selectedTime } TimePickerDialog( - millisOfDay = time, - is24Hour = remember { requireContext().is24HourFormat }, + state = rememberTimePickerState( + initialHour = time / (60 * 60_000), + initialMinute = (time / (60_000)) % 60, + is24Hour = requireContext().is24HourFormat + ), initialDisplayMode = remember { preferences.timeDisplayMode }, setDisplayMode = { preferences.timeDisplayMode = it }, selected = { returnSelectedTime(it + 1000) },