Moving logic into composables, adding previews

pull/1952/head
Alex Baker 2 years ago
parent 5c3af50c9d
commit 3e3de3c1d6

@ -8,24 +8,17 @@ package com.todoroo.astrid.repeats
import android.app.Activity.RESULT_OK
import android.content.Intent
import android.os.Bundle
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.lifecycle.lifecycleScope
import com.google.android.material.composethemeadapter.MdcTheme
import dagger.hilt.android.AndroidEntryPoint
import net.fortuna.ical4j.model.Recur
import net.fortuna.ical4j.model.WeekDay
import org.tasks.R
import org.tasks.compose.DisabledText
import org.tasks.compose.collectAsStateLifecycleAware
import org.tasks.compose.edit.RepeatRow
import org.tasks.repeats.BasicRecurrenceDialog
import org.tasks.repeats.RecurrenceUtils.newRecur
import org.tasks.repeats.RepeatRuleToString
@ -53,7 +46,8 @@ class RepeatControlSet : TaskEditControlComposeFragment() {
val recur = newRecur(recurrence)
if (recur.frequency == Recur.Frequency.MONTHLY && recur.dayList.isNotEmpty()) {
val weekdayNum = recur.dayList[0]
val dateTime = DateTime(this.dueDate)
val dateTime =
DateTime(this.viewModel.dueDate.value.let { if (it > 0) it else currentTimeMillis() })
val num: Int
val dayOfWeekInMonth = dateTime.dayOfWeekInMonth
num = if (weekdayNum.offset == -1 || dayOfWeekInMonth == 5) {
@ -78,29 +72,29 @@ class RepeatControlSet : TaskEditControlComposeFragment() {
}
}
private val dueDate: Long
get() = viewModel.dueDate.value.let { if (it > 0) it else currentTimeMillis() }
override fun onRowClick() {
BasicRecurrenceDialog.newBasicRecurrenceDialog(
this, REQUEST_RECURRENCE, viewModel.recurrence.value, dueDate)
.show(parentFragmentManager, FRAG_TAG_BASIC_RECURRENCE)
}
override val isClickable = true
@Composable
override fun Body() {
RepeatRow(
recurrence = viewModel.recurrence.collectAsStateLifecycleAware().value?.let {
repeatRuleToString.toString(it)
},
repeatFromCompletion = viewModel.repeatAfterCompletion.collectAsStateLifecycleAware().value,
onRepeatFromChanged = { viewModel.repeatAfterCompletion.value = it }
)
}
override val icon = R.drawable.ic_outline_repeat_24px
override fun bind(parent: ViewGroup?): View =
(parent as ComposeView).apply {
setContent {
MdcTheme {
RepeatRow(
recurrence = viewModel.recurrence.collectAsStateLifecycleAware().value?.let {
repeatRuleToString.toString(it)
},
repeatAfterCompletion = viewModel.repeatAfterCompletion.collectAsStateLifecycleAware().value,
onClick = {
BasicRecurrenceDialog.newBasicRecurrenceDialog(
this@RepeatControlSet,
REQUEST_RECURRENCE,
viewModel.recurrence.value,
viewModel.dueDate.value.let { if (it > 0) it else currentTimeMillis() }
)
.show(parentFragmentManager, FRAG_TAG_BASIC_RECURRENCE)
},
onRepeatFromChanged = { viewModel.repeatAfterCompletion.value = it }
)
}
}
}
override fun controlId() = TAG
@ -110,59 +104,3 @@ class RepeatControlSet : TaskEditControlComposeFragment() {
private const val REQUEST_RECURRENCE = 10000
}
}
@Composable
fun RepeatRow(
recurrence: String?,
repeatFromCompletion: Boolean,
onRepeatFromChanged: (Boolean) -> Unit,
) {
Column {
Spacer(modifier = Modifier.height(20.dp))
if (recurrence.isNullOrBlank()) {
DisabledText(text = stringResource(id = R.string.repeat_option_does_not_repeat))
} else {
Text(
text = recurrence,
modifier = Modifier.height(24.dp)
)
Spacer(modifier = Modifier.height(8.dp))
Row {
Text(text = stringResource(id = R.string.repeats_from))
Spacer(modifier = Modifier.width(4.dp))
var expanded by remember { mutableStateOf(false) }
Text(
text = stringResource(
id = if (repeatFromCompletion)
R.string.repeat_type_completion
else
R.string.repeat_type_due
),
style = MaterialTheme.typography.body1.copy(
textDecoration = TextDecoration.Underline,
),
modifier = Modifier.clickable { expanded = true }
)
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
DropdownMenuItem(
onClick = {
expanded = false
onRepeatFromChanged(false)
}
) {
Text(text = stringResource(id = R.string.repeat_type_due))
}
DropdownMenuItem(
onClick = {
expanded = false
onRepeatFromChanged(true)
}
) {
Text(text = stringResource(id = R.string.repeat_type_completion))
}
}
}
}
Spacer(modifier = Modifier.height(20.dp))
}
}

@ -3,47 +3,26 @@ package com.todoroo.astrid.ui
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import android.view.ViewGroup
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ContentAlpha
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import com.google.android.material.composethemeadapter.MdcTheme
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.andlib.utility.DateUtilities.getTimeString
import com.todoroo.andlib.utility.DateUtilities.now
import com.todoroo.astrid.ui.StartDateControlSet.Companion.getRelativeDateString
import dagger.hilt.android.AndroidEntryPoint
import org.tasks.R
import org.tasks.compose.TaskEditIcon
import org.tasks.compose.TaskEditRow
import org.tasks.compose.collectAsStateLifecycleAware
import org.tasks.compose.edit.StartDateRow
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.dialogs.StartDatePicker
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.EXTRA_DAY
import org.tasks.dialogs.StartDatePicker.Companion.EXTRA_TIME
import org.tasks.dialogs.StartDatePicker.Companion.NO_DAY
import org.tasks.dialogs.StartDatePicker.Companion.NO_TIME
import org.tasks.dialogs.StartDatePicker.Companion.WEEK_BEFORE_DUE
import org.tasks.preferences.Preferences
import org.tasks.ui.TaskEditControlComposeFragment
import org.tasks.ui.TaskEditViewModel
import java.time.format.FormatStyle
import java.util.*
import javax.inject.Inject
@ -70,11 +49,23 @@ class StartDateControlSet : TaskEditControlComposeFragment() {
(parent as ComposeView).apply {
setContent {
MdcTheme {
val selectedDay = vm.selectedDay.collectAsStateLifecycleAware().value
val selectedTime = vm.selectedTime.collectAsStateLifecycleAware().value
StartDateRow(
viewModel = viewModel,
vm = vm,
preferences = preferences,
locale = locale,
startDate = viewModel.startDate.collectAsStateLifecycleAware().value,
selectedDay = selectedDay,
selectedTime = selectedTime,
hasDueDate = viewModel.dueDate.collectAsStateLifecycleAware().value > 0,
printDate = {
DateUtilities.getRelativeDateTime(
context,
selectedDay + selectedTime,
locale,
FormatStyle.FULL,
preferences.alwaysDisplayFullDate,
false
)
},
onClick = {
val fragmentManager = parentFragmentManager
if (fragmentManager.findFragmentByTag(FRAG_TAG_DATE_PICKER) == null) {
@ -130,125 +121,3 @@ class StartDateControlSet : TaskEditControlComposeFragment() {
}
}
}
@Composable
fun StartDateRow(
viewModel: TaskEditViewModel,
vm: StartDateViewModel,
preferences: Preferences,
locale: Locale,
onClick: () -> Unit,
) {
TaskEditRow(
icon = {
TaskEditIcon(
id = R.drawable.ic_pending_actions_24px,
modifier = Modifier
.padding(
start = 16.dp,
top = 20.dp,
end = 32.dp,
bottom = 20.dp
)
)
},
content = {
StartDate(
startDate = viewModel.startDate.collectAsStateLifecycleAware().value,
selectedDay = vm.selectedDay.collectAsStateLifecycleAware().value,
selectedTime = vm.selectedTime.collectAsStateLifecycleAware().value,
displayFullDate = preferences.alwaysDisplayFullDate,
locale = locale,
hasDueDate = viewModel.dueDate.collectAsStateLifecycleAware().value > 0
)
},
onClick = onClick
)
}
@Composable
fun StartDate(
startDate: Long,
selectedDay: Long,
selectedTime: Int,
displayFullDate: Boolean,
locale: Locale = Locale.getDefault(),
currentTime: Long = now(),
hasDueDate: Boolean,
) {
val context = LocalContext.current
Text(
text = when (selectedDay) {
DUE_DATE -> context.getRelativeDateString(R.string.due_date, selectedTime)
DUE_TIME -> context.getString(R.string.due_time)
DAY_BEFORE_DUE -> context.getRelativeDateString(R.string.day_before_due, selectedTime)
WEEK_BEFORE_DUE -> context.getRelativeDateString(R.string.week_before_due, selectedTime)
in 1..Long.MAX_VALUE -> DateUtilities.getRelativeDateTime(
LocalContext.current,
selectedDay + selectedTime,
locale,
FormatStyle.FULL,
displayFullDate,
false
)
else -> stringResource(id = R.string.no_start_date)
},
color = when {
selectedDay < 0 && !hasDueDate -> colorResource(id = R.color.overdue)
startDate == 0L -> MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled)
startDate < currentTime -> colorResource(id = R.color.overdue)
else -> MaterialTheme.colors.onSurface
},
modifier = Modifier
.padding(vertical = 20.dp)
.height(24.dp),
)
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun NoStartDate() {
MdcTheme {
StartDate(
startDate = 0L,
selectedDay = NO_DAY,
selectedTime = NO_TIME,
displayFullDate = false,
currentTime = 1657080392000L,
hasDueDate = false,
)
}
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun FutureStartDate() {
MdcTheme {
StartDate(
startDate = 1657080392000L,
selectedDay = DUE_DATE,
selectedTime = NO_TIME,
displayFullDate = false,
currentTime = 1657080392000L,
hasDueDate = false,
)
}
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun PastStartDate() {
MdcTheme {
StartDate(
startDate = 1657080392000L,
selectedDay = DUE_TIME,
selectedTime = NO_TIME,
displayFullDate = false,
currentTime = 1657080392001L,
hasDueDate = false,
)
}
}

@ -1,6 +1,7 @@
package org.tasks.compose
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ContentAlpha
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
@ -19,6 +20,7 @@ fun DisabledText(
style = MaterialTheme.typography.body1,
modifier = modifier
.alpha(alpha = ContentAlpha.disabled)
.height(24.dp),
.padding(end = 16.dp)
.defaultMinSize(minHeight = 24.dp),
)
}

@ -2,12 +2,29 @@ package org.tasks.compose
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ContentAlpha
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.unit.dp
@Composable
fun TaskEditRow(
icon: @Composable () -> Unit,
iconRes: Int = 0,
icon: @Composable () -> Unit = {
TaskEditIcon(
id = iconRes,
modifier = Modifier
.alpha(ContentAlpha.medium)
.padding(
start = 16.dp,
top = 20.dp,
end = 32.dp,
bottom = 20.dp
)
)
},
content: @Composable () -> Unit,
onClick: (() -> Unit)? = null,
) {

@ -0,0 +1,81 @@
package org.tasks.compose.edit
import android.content.res.Configuration
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.google.android.material.composethemeadapter.MdcTheme
import org.tasks.R
import org.tasks.compose.DisabledText
import org.tasks.compose.TaskEditRow
@Composable
fun DueDateRow(
dueDate: String?,
overdue: Boolean,
onClick: () -> Unit,
) {
TaskEditRow(
iconRes = R.drawable.ic_outline_schedule_24px,
content = {
DueDate(
dueDate = dueDate,
overdue = overdue,
)
},
onClick = onClick,
)
}
@Composable
fun DueDate(dueDate: String?, overdue: Boolean) {
if (dueDate.isNullOrBlank()) {
DisabledText(
text = stringResource(id = R.string.no_due_date),
modifier = Modifier.padding(top = 20.dp, bottom = 20.dp, end = 16.dp)
)
} else {
Text(
text = dueDate,
color = if (overdue) {
colorResource(id = R.color.overdue)
} else {
MaterialTheme.colors.onSurface
},
modifier = Modifier.padding(top = 20.dp, bottom = 20.dp, end = 16.dp)
)
}
}
@ExperimentalComposeUiApi
@Preview(showBackground = true, widthDp = 320)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 320)
@Composable
fun DueDatePreview() {
MdcTheme {
DueDateRow(
dueDate = "Today",
overdue = false,
) {}
}
}
@ExperimentalComposeUiApi
@Preview(showBackground = true, widthDp = 320)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 320)
@Composable
fun NoDueDatePreview() {
MdcTheme {
DueDateRow(
dueDate = null,
overdue = false,
) {}
}
}

@ -0,0 +1,123 @@
package org.tasks.compose.edit
import android.content.res.Configuration
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ContentAlpha
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Notifications
import androidx.compose.material.icons.outlined.NotificationsOff
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.google.android.material.composethemeadapter.MdcTheme
import org.tasks.R
import org.tasks.compose.DisabledText
import org.tasks.compose.TaskEditRow
import org.tasks.data.Geofence
import org.tasks.data.Location
import org.tasks.data.Place
@Composable
fun LocationRow(
location: Location?,
hasPermissions: Boolean,
onClick: () -> Unit,
openGeofenceOptions: () -> Unit,
) {
TaskEditRow(
iconRes = R.drawable.ic_outline_place_24px,
content = {
if (location == null) {
DisabledText(
text = stringResource(id = R.string.add_location),
modifier = Modifier.padding(vertical = 20.dp)
)
} else {
Location(
name = location.displayName,
address = location.displayAddress,
openGeofenceOptions = openGeofenceOptions,
geofenceOn = hasPermissions && (location.isArrival || location.isDeparture)
)
}
},
onClick = onClick
)
}
@Composable
fun Location(
name: String,
address: String?,
geofenceOn: Boolean,
openGeofenceOptions: () -> Unit,
) {
Row {
Column(
modifier = Modifier
.weight(1f)
.padding(vertical = 20.dp)
) {
Text(text = name)
address?.takeIf { it.isNotBlank() && it != name }?.let {
Text(text = address)
}
}
IconButton(
onClick = openGeofenceOptions,
modifier = Modifier.padding(top = 8.dp /* + 12dp from icon */)
) {
Icon(
imageVector = if (geofenceOn) {
Icons.Outlined.Notifications
} else {
Icons.Outlined.NotificationsOff
},
contentDescription = null,
modifier = Modifier.alpha(ContentAlpha.medium),
)
}
}
}
@Preview(showBackground = true, widthDp = 320)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 320)
@Composable
fun NoLocation() {
MdcTheme {
LocationRow(
location = null,
hasPermissions = true,
onClick = {},
openGeofenceOptions = {},
)
}
}
@Preview(showBackground = true, widthDp = 320)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 320)
@Composable
fun SampleLocation() {
MdcTheme {
LocationRow(
location = Location(
Geofence(),
Place().apply {
name = "Googleplex"
address = "1600 Amphitheatre Pkwy, Mountain View, CA 94043"
},
),
hasPermissions = true,
onClick = {},
openGeofenceOptions = {},
)
}
}

@ -0,0 +1,96 @@
package org.tasks.compose.edit
import android.content.res.Configuration
import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.RadioButton
import androidx.compose.material.RadioButtonDefaults
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.google.android.material.composethemeadapter.MdcTheme
import com.todoroo.astrid.data.Task
import org.tasks.R
import org.tasks.compose.TaskEditRow
@Composable
fun PriorityRow(
priority: Int,
onChangePriority: (Int) -> Unit,
) {
TaskEditRow(
iconRes = R.drawable.ic_outline_flag_24px,
content = {
Priority(
selected = priority,
onClick = { onChangePriority(it) }
)
},
)
}
@Composable
fun Priority(
selected: Int,
onClick: (Int) -> Unit = {}
) {
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(
top = dimensionResource(id = R.dimen.half_keyline_first),
bottom = dimensionResource(id = R.dimen.half_keyline_first),
end = 16.dp
),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(id = R.string.TEA_importance_label),
style = MaterialTheme.typography.body1,
)
Spacer(modifier = Modifier.weight(1f))
for (i in Task.Priority.HIGH..Task.Priority.NONE) {
PriorityButton(priority = i, selected = selected, onClick = onClick)
}
}
}
@Composable
fun PriorityButton(
@Task.Priority priority: Int,
selected: Int,
onClick: (Int) -> Unit,
) {
val color = when (priority) {
in Int.MIN_VALUE..Task.Priority.HIGH -> colorResource(id = R.color.red_500)
Task.Priority.MEDIUM -> colorResource(id = R.color.amber_500)
Task.Priority.LOW -> colorResource(id = R.color.blue_500)
else -> colorResource(R.color.grey_500)
}
RadioButton(
selected = priority == selected,
onClick = { onClick(priority) },
colors = RadioButtonDefaults.colors(
selectedColor = color,
unselectedColor = color,
),
)
}
@ExperimentalComposeUiApi
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun PriorityPreview() {
MdcTheme {
PriorityRow(priority = Task.Priority.MEDIUM) {}
}
}

@ -0,0 +1,127 @@
package org.tasks.compose.edit
import android.content.res.Configuration
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.*
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.google.android.material.composethemeadapter.MdcTheme
import org.tasks.R
import org.tasks.compose.DisabledText
import org.tasks.compose.TaskEditRow
@Composable
fun RepeatRow(
recurrence: String?,
repeatAfterCompletion: Boolean,
onClick: () -> Unit,
onRepeatFromChanged: (Boolean) -> Unit,
) {
TaskEditRow(
iconRes = R.drawable.ic_outline_repeat_24px,
content = {
Repeat(
recurrence = recurrence,
repeatFromCompletion = repeatAfterCompletion,
onRepeatFromChanged = onRepeatFromChanged,
)
},
onClick = onClick,
)
}
@Composable
fun Repeat(
recurrence: String?,
repeatFromCompletion: Boolean,
onRepeatFromChanged: (Boolean) -> Unit,
) {
Column {
Spacer(modifier = Modifier.height(20.dp))
if (recurrence.isNullOrBlank()) {
DisabledText(text = stringResource(id = R.string.repeat_option_does_not_repeat))
} else {
Text(
text = recurrence,
modifier = Modifier.defaultMinSize(minHeight = 24.dp).padding(end = 16.dp).fillMaxWidth(),
maxLines = Int.MAX_VALUE,
)
Spacer(modifier = Modifier.height(8.dp))
Row {
Text(text = stringResource(id = R.string.repeats_from))
Spacer(modifier = Modifier.width(4.dp))
var expanded by remember { mutableStateOf(false) }
Text(
text = stringResource(
id = if (repeatFromCompletion)
R.string.repeat_type_completion
else
R.string.repeat_type_due
),
style = MaterialTheme.typography.body1.copy(
textDecoration = TextDecoration.Underline,
),
modifier = Modifier.clickable { expanded = true }
)
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
DropdownMenuItem(
onClick = {
expanded = false
onRepeatFromChanged(false)
}
) {
Text(text = stringResource(id = R.string.repeat_type_due))
}
DropdownMenuItem(
onClick = {
expanded = false
onRepeatFromChanged(true)
}
) {
Text(text = stringResource(id = R.string.repeat_type_completion))
}
}
}
}
Spacer(modifier = Modifier.height(20.dp))
}
}
@ExperimentalComposeUiApi
@Preview(showBackground = true, widthDp = 320)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 320)
@Composable
fun RepeatPreview() {
MdcTheme {
RepeatRow(
recurrence = "Repeats weekly on Mon, Tue, Wed, Thu, Fri",
repeatAfterCompletion = false,
onClick = {},
onRepeatFromChanged = {},
)
}
}
@ExperimentalComposeUiApi
@Preview(showBackground = true, widthDp = 320)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 320)
@Composable
fun NoRepeatPreview() {
MdcTheme {
RepeatRow(
recurrence = null,
repeatAfterCompletion = false,
onClick = {},
onRepeatFromChanged = {},
)
}
}

@ -0,0 +1,129 @@
package org.tasks.compose.edit
import android.content.res.Configuration
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ContentAlpha
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.google.android.material.composethemeadapter.MdcTheme
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.ui.StartDateControlSet.Companion.getRelativeDateString
import org.tasks.R
import org.tasks.compose.TaskEditRow
import org.tasks.dialogs.StartDatePicker
@Composable
fun StartDateRow(
startDate: Long,
selectedDay: Long,
selectedTime: Int,
currentTime: Long = DateUtilities.now(),
hasDueDate: Boolean,
printDate: () -> String,
onClick: () -> Unit,
) {
TaskEditRow(
iconRes = R.drawable.ic_pending_actions_24px,
content = {
StartDate(
startDate = startDate,
selectedDay = selectedDay,
selectedTime = selectedTime,
currentTime = currentTime,
hasDueDate = hasDueDate,
printDate = printDate,
)
},
onClick = onClick
)
}
@Composable
fun StartDate(
startDate: Long,
selectedDay: Long,
selectedTime: Int,
currentTime: Long,
hasDueDate: Boolean,
printDate: () -> String,
) {
val context = LocalContext.current
Text(
text = when (selectedDay) {
StartDatePicker.DUE_DATE -> context.getRelativeDateString(R.string.due_date, selectedTime)
StartDatePicker.DUE_TIME -> context.getString(R.string.due_time)
StartDatePicker.DAY_BEFORE_DUE -> context.getRelativeDateString(R.string.day_before_due, selectedTime)
StartDatePicker.WEEK_BEFORE_DUE -> context.getRelativeDateString(R.string.week_before_due, selectedTime)
in 1..Long.MAX_VALUE -> printDate()
else -> stringResource(id = R.string.no_start_date)
},
color = when {
selectedDay < 0 && !hasDueDate -> colorResource(id = R.color.overdue)
startDate == 0L -> MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.disabled)
startDate < currentTime -> colorResource(id = R.color.overdue)
else -> MaterialTheme.colors.onSurface
},
modifier = Modifier
.padding(vertical = 20.dp)
.height(24.dp),
)
}
@Preview(showBackground = true, widthDp = 320)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 320)
@Composable
fun NoStartDate() {
MdcTheme {
StartDateRow(
startDate = 0L,
selectedDay = StartDatePicker.NO_DAY,
selectedTime = StartDatePicker.NO_TIME,
currentTime = 1657080392000L,
hasDueDate = false,
printDate = { "" },
onClick = {},
)
}
}
@Preview(showBackground = true, widthDp = 320)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 320)
@Composable
fun FutureStartDate() {
MdcTheme {
StartDateRow(
startDate = 1657080392000L,
selectedDay = StartDatePicker.DUE_DATE,
selectedTime = StartDatePicker.NO_TIME,
currentTime = 1657080392000L,
hasDueDate = false,
printDate = { "" },
onClick = {},
)
}
}
@Preview(showBackground = true, widthDp = 320)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 320)
@Composable
fun PastStartDate() {
MdcTheme {
StartDateRow(
startDate = 1657080392000L,
selectedDay = StartDatePicker.DUE_TIME,
selectedTime = StartDatePicker.NO_TIME,
currentTime = 1657080392001L,
hasDueDate = false,
printDate = { "" },
onClick = {},
)
}
}

@ -3,29 +3,18 @@ package org.tasks.ui
import android.app.Activity
import android.content.Intent
import android.view.ViewGroup
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.google.android.material.composethemeadapter.MdcTheme
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.data.Task.Companion.hasDueTime
import dagger.hilt.android.AndroidEntryPoint
import org.tasks.R
import org.tasks.compose.DisabledText
import org.tasks.compose.TaskEditIcon
import org.tasks.compose.TaskEditRow
import org.tasks.compose.collectAsStateLifecycleAware
import org.tasks.compose.edit.DueDateRow
import org.tasks.date.DateTimeUtils
import org.tasks.dialogs.DateTimePicker
import org.tasks.preferences.Preferences
import org.tasks.ui.DeadlineControlSet.Companion.isOverdue
import java.time.format.FormatStyle
import java.util.*
import javax.inject.Inject
@ -39,10 +28,21 @@ class DeadlineControlSet : TaskEditControlComposeFragment() {
(parent as ComposeView).apply {
setContent {
MdcTheme {
val dueDate = viewModel.dueDate.collectAsStateLifecycleAware().value
DueDateRow(
viewModel = viewModel,
locale = locale,
displayFullDate = preferences.alwaysDisplayFullDate,
dueDate = if (dueDate == 0L) {
null
} else {
DateUtilities.getRelativeDateTime(
LocalContext.current,
dueDate,
locale,
FormatStyle.FULL,
preferences.alwaysDisplayFullDate,
false
)
},
overdue = dueDate.isOverdue,
onClick = {
val fragmentManager = parentFragmentManager
if (fragmentManager.findFragmentByTag(FRAG_TAG_DATE_PICKER) == null) {
@ -84,60 +84,3 @@ class DeadlineControlSet : TaskEditControlComposeFragment() {
}
}
}
@Composable
fun DueDateRow(
viewModel: TaskEditViewModel,
locale: Locale,
displayFullDate: Boolean,
onClick: () -> Unit,
) {
TaskEditRow(
icon = {
TaskEditIcon(
id = R.drawable.ic_outline_schedule_24px,
modifier = Modifier.padding(
start = 16.dp,
top = 20.dp,
end = 32.dp,
bottom = 20.dp
)
)
},
content = {
DueDate(
dueDate = viewModel.dueDate.collectAsStateLifecycleAware().value,
locale = locale,
displayFullDate = displayFullDate,
)
},
onClick = onClick,
)
}
@Composable
fun DueDate(dueDate: Long, locale: Locale, displayFullDate: Boolean) {
if (dueDate == 0L) {
DisabledText(
text = stringResource(id = R.string.no_due_date),
modifier = Modifier.padding(vertical = 20.dp)
)
} else {
Text(
text = DateUtilities.getRelativeDateTime(
LocalContext.current,
dueDate,
locale,
FormatStyle.FULL,
displayFullDate,
false
),
color = if (dueDate.isOverdue) {
colorResource(id = R.color.overdue)
} else {
MaterialTheme.colors.onSurface
},
modifier = Modifier.padding(vertical = 20.dp)
)
}
}

@ -4,29 +4,18 @@ import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Parcelable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ContentAlpha
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Notifications
import androidx.compose.material.icons.outlined.NotificationsOff
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import androidx.core.util.Pair
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.google.android.material.composethemeadapter.MdcTheme
import dagger.hilt.android.AndroidEntryPoint
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.compose.DisabledText
import org.tasks.compose.collectAsStateLifecycleAware
import org.tasks.compose.edit.LocationRow
import org.tasks.data.Geofence
import org.tasks.data.Location
import org.tasks.data.Place
@ -35,8 +24,6 @@ import org.tasks.dialogs.GeofenceDialog
import org.tasks.extensions.Context.openUri
import org.tasks.location.LocationPermissionDialog.Companion.newLocationPermissionDialog
import org.tasks.location.LocationPickerActivity
import org.tasks.preferences.Device
import org.tasks.preferences.FragmentPermissionRequestor
import org.tasks.preferences.PermissionChecker
import org.tasks.preferences.PermissionChecker.backgroundPermissions
import org.tasks.preferences.Preferences
@ -46,8 +33,6 @@ import javax.inject.Inject
class LocationControlSet : TaskEditControlComposeFragment() {
@Inject lateinit var preferences: Preferences
@Inject lateinit var dialogBuilder: DialogBuilder
@Inject lateinit var device: Device
@Inject lateinit var permissionRequestor: FragmentPermissionRequestor
@Inject lateinit var permissionChecker: PermissionChecker
private fun setLocation(location: Location?) {
@ -94,40 +79,35 @@ class LocationControlSet : TaskEditControlComposeFragment() {
}
@OptIn(ExperimentalPermissionsApi::class)
@Composable
override fun Body() {
val location = viewModel.selectedLocation.collectAsStateLifecycleAware().value
val hasPermissions =
rememberMultiplePermissionsState(permissions = backgroundPermissions())
.allPermissionsGranted
if (location == null) {
DisabledText(
text = stringResource(id = R.string.add_location),
modifier = Modifier.padding(vertical = 20.dp)
)
} else {
LocationRow(
name = location.displayName,
address = location.displayAddress,
onClick = {
if (hasPermissions) {
showGeofenceOptions()
} else {
newLocationPermissionDialog(this, REQUEST_LOCATION_PERMISSIONS)
.show(parentFragmentManager, FRAG_TAG_REQUEST_LOCATION)
}
},
geofenceOn = hasPermissions && (location.isArrival || location.isDeparture)
)
override fun bind(parent: ViewGroup?): View =
(parent as ComposeView).apply {
setContent {
MdcTheme {
val hasPermissions =
rememberMultiplePermissionsState(permissions = backgroundPermissions())
.allPermissionsGranted
LocationRow(
location = viewModel.selectedLocation.collectAsStateLifecycleAware().value,
hasPermissions = hasPermissions,
onClick = this@LocationControlSet::onRowClick,
openGeofenceOptions = {
if (hasPermissions) {
showGeofenceOptions()
} else {
newLocationPermissionDialog(
this@LocationControlSet,
REQUEST_LOCATION_PERMISSIONS
)
.show(parentFragmentManager, FRAG_TAG_REQUEST_LOCATION)
}
}
)
}
}
}
}
override val icon = R.drawable.ic_outline_place_24px
override fun controlId() = TAG
override val isClickable = true
private fun openWebsite() {
viewModel.selectedLocation.value?.let { context?.openUri(it.url) }
}
@ -180,39 +160,3 @@ class LocationControlSet : TaskEditControlComposeFragment() {
private const val FRAG_TAG_REQUEST_LOCATION = "request_location"
}
}
@Composable
fun LocationRow(
name: String,
address: String?,
geofenceOn: Boolean,
onClick: () -> Unit,
) {
Row {
Column(
modifier = Modifier
.weight(1f)
.padding(vertical = 20.dp)
) {
Text(text = name)
address?.takeIf { it.isNotBlank() && it != name }?.let {
Text(text = address)
}
}
IconButton(
onClick = onClick,
modifier = Modifier.padding(top = 8.dp /* + 12dp from icon */)
) {
Icon(
imageVector = if (geofenceOn) {
Icons.Outlined.Notifications
} else {
Icons.Outlined.NotificationsOff
},
contentDescription = null,
modifier = Modifier.alpha(ContentAlpha.medium),
)
}
}
}

@ -1,38 +1,28 @@
package org.tasks.ui
import android.content.res.Configuration
import androidx.compose.foundation.layout.*
import androidx.compose.material.MaterialTheme
import androidx.compose.material.RadioButton
import androidx.compose.material.RadioButtonDefaults
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import android.view.View
import android.view.ViewGroup
import androidx.compose.ui.platform.ComposeView
import com.google.android.material.composethemeadapter.MdcTheme
import com.todoroo.astrid.data.Task
import dagger.hilt.android.AndroidEntryPoint
import org.tasks.R
import org.tasks.compose.collectAsStateLifecycleAware
import org.tasks.compose.edit.PriorityRow
@AndroidEntryPoint
class PriorityControlSet : TaskEditControlComposeFragment() {
@Composable
override fun Body() {
val priority = viewModel.priority.collectAsStateLifecycleAware()
PriorityRow(
selected = priority.value,
onClick = { viewModel.priority.value = it })
}
override val icon = R.drawable.ic_outline_flag_24px
override fun bind(parent: ViewGroup?): View =
(parent as ComposeView).apply {
setContent {
MdcTheme {
PriorityRow(
priority = viewModel.priority.collectAsStateLifecycleAware().value,
onChangePriority = { viewModel.priority.value = it }
)
}
}
}
override fun controlId() = TAG
@ -40,62 +30,3 @@ class PriorityControlSet : TaskEditControlComposeFragment() {
const val TAG = R.string.TEA_ctrl_importance_pref
}
}
@Composable
fun PriorityRow(
selected: Int,
onClick: (Int) -> Unit = {}
) {
Row(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.padding(
top = dimensionResource(id = R.dimen.half_keyline_first),
bottom = dimensionResource(id = R.dimen.half_keyline_first),
end = 16.dp
),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(id = R.string.TEA_importance_label),
style = MaterialTheme.typography.body1,
)
Spacer(modifier = Modifier.weight(1f))
for (i in Task.Priority.HIGH..Task.Priority.NONE) {
PriorityButton(priority = i, selected = selected, onClick = onClick)
}
}
}
@Composable
fun PriorityButton(
@Task.Priority priority: Int,
selected: Int,
onClick: (Int) -> Unit,
) {
val color = when (priority) {
in Int.MIN_VALUE..Task.Priority.HIGH -> colorResource(id = R.color.red_500)
Task.Priority.MEDIUM -> colorResource(id = R.color.amber_500)
Task.Priority.LOW -> colorResource(id = R.color.blue_500)
else -> colorResource(R.color.grey_500)
}
RadioButton(
selected = priority == selected,
onClick = { onClick(priority) },
colors = RadioButtonDefaults.colors(
selectedColor = color,
unselectedColor = color,
),
)
}
@ExperimentalComposeUiApi
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun PriorityPreview() {
MdcTheme {
PriorityRow(selected = Task.Priority.MEDIUM)
}
}
Loading…
Cancel
Save