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.app.Activity.RESULT_OK
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.compose.foundation.clickable import android.view.View
import androidx.compose.foundation.layout.* import android.view.ViewGroup
import androidx.compose.material.DropdownMenu import androidx.compose.ui.platform.ComposeView
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 androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.composethemeadapter.MdcTheme
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import net.fortuna.ical4j.model.Recur import net.fortuna.ical4j.model.Recur
import net.fortuna.ical4j.model.WeekDay import net.fortuna.ical4j.model.WeekDay
import org.tasks.R import org.tasks.R
import org.tasks.compose.DisabledText
import org.tasks.compose.collectAsStateLifecycleAware import org.tasks.compose.collectAsStateLifecycleAware
import org.tasks.compose.edit.RepeatRow
import org.tasks.repeats.BasicRecurrenceDialog import org.tasks.repeats.BasicRecurrenceDialog
import org.tasks.repeats.RecurrenceUtils.newRecur import org.tasks.repeats.RecurrenceUtils.newRecur
import org.tasks.repeats.RepeatRuleToString import org.tasks.repeats.RepeatRuleToString
@ -53,7 +46,8 @@ class RepeatControlSet : TaskEditControlComposeFragment() {
val recur = newRecur(recurrence) val recur = newRecur(recurrence)
if (recur.frequency == Recur.Frequency.MONTHLY && recur.dayList.isNotEmpty()) { if (recur.frequency == Recur.Frequency.MONTHLY && recur.dayList.isNotEmpty()) {
val weekdayNum = recur.dayList[0] 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 num: Int
val dayOfWeekInMonth = dateTime.dayOfWeekInMonth val dayOfWeekInMonth = dateTime.dayOfWeekInMonth
num = if (weekdayNum.offset == -1 || dayOfWeekInMonth == 5) { num = if (weekdayNum.offset == -1 || dayOfWeekInMonth == 5) {
@ -78,29 +72,29 @@ class RepeatControlSet : TaskEditControlComposeFragment() {
} }
} }
private val dueDate: Long override fun bind(parent: ViewGroup?): View =
get() = viewModel.dueDate.value.let { if (it > 0) it else currentTimeMillis() } (parent as ComposeView).apply {
setContent {
override fun onRowClick() { MdcTheme {
BasicRecurrenceDialog.newBasicRecurrenceDialog( RepeatRow(
this, REQUEST_RECURRENCE, viewModel.recurrence.value, dueDate) recurrence = viewModel.recurrence.collectAsStateLifecycleAware().value?.let {
.show(parentFragmentManager, FRAG_TAG_BASIC_RECURRENCE) repeatRuleToString.toString(it)
} },
repeatAfterCompletion = viewModel.repeatAfterCompletion.collectAsStateLifecycleAware().value,
override val isClickable = true onClick = {
BasicRecurrenceDialog.newBasicRecurrenceDialog(
@Composable this@RepeatControlSet,
override fun Body() { REQUEST_RECURRENCE,
RepeatRow( viewModel.recurrence.value,
recurrence = viewModel.recurrence.collectAsStateLifecycleAware().value?.let { viewModel.dueDate.value.let { if (it > 0) it else currentTimeMillis() }
repeatRuleToString.toString(it) )
}, .show(parentFragmentManager, FRAG_TAG_BASIC_RECURRENCE)
repeatFromCompletion = viewModel.repeatAfterCompletion.collectAsStateLifecycleAware().value, },
onRepeatFromChanged = { viewModel.repeatAfterCompletion.value = it } onRepeatFromChanged = { viewModel.repeatAfterCompletion.value = it }
) )
} }
}
override val icon = R.drawable.ic_outline_repeat_24px }
override fun controlId() = TAG override fun controlId() = TAG
@ -110,59 +104,3 @@ class RepeatControlSet : TaskEditControlComposeFragment() {
private const val REQUEST_RECURRENCE = 10000 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.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle import android.os.Bundle
import android.view.ViewGroup 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.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.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.composethemeadapter.MdcTheme import com.google.android.material.composethemeadapter.MdcTheme
import com.todoroo.andlib.utility.DateUtilities import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.andlib.utility.DateUtilities.getTimeString 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 dagger.hilt.android.AndroidEntryPoint
import org.tasks.R import org.tasks.R
import org.tasks.compose.TaskEditIcon
import org.tasks.compose.TaskEditRow
import org.tasks.compose.collectAsStateLifecycleAware import org.tasks.compose.collectAsStateLifecycleAware
import org.tasks.compose.edit.StartDateRow
import org.tasks.date.DateTimeUtils.newDateTime import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.dialogs.StartDatePicker 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_DAY
import org.tasks.dialogs.StartDatePicker.Companion.EXTRA_TIME import org.tasks.dialogs.StartDatePicker.Companion.EXTRA_TIME
import org.tasks.dialogs.StartDatePicker.Companion.NO_DAY import org.tasks.dialogs.StartDatePicker.Companion.NO_DAY
import org.tasks.dialogs.StartDatePicker.Companion.NO_TIME 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.preferences.Preferences
import org.tasks.ui.TaskEditControlComposeFragment import org.tasks.ui.TaskEditControlComposeFragment
import org.tasks.ui.TaskEditViewModel
import java.time.format.FormatStyle import java.time.format.FormatStyle
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
@ -70,11 +49,23 @@ class StartDateControlSet : TaskEditControlComposeFragment() {
(parent as ComposeView).apply { (parent as ComposeView).apply {
setContent { setContent {
MdcTheme { MdcTheme {
val selectedDay = vm.selectedDay.collectAsStateLifecycleAware().value
val selectedTime = vm.selectedTime.collectAsStateLifecycleAware().value
StartDateRow( StartDateRow(
viewModel = viewModel, startDate = viewModel.startDate.collectAsStateLifecycleAware().value,
vm = vm, selectedDay = selectedDay,
preferences = preferences, selectedTime = selectedTime,
locale = locale, hasDueDate = viewModel.dueDate.collectAsStateLifecycleAware().value > 0,
printDate = {
DateUtilities.getRelativeDateTime(
context,
selectedDay + selectedTime,
locale,
FormatStyle.FULL,
preferences.alwaysDisplayFullDate,
false
)
},
onClick = { onClick = {
val fragmentManager = parentFragmentManager val fragmentManager = parentFragmentManager
if (fragmentManager.findFragmentByTag(FRAG_TAG_DATE_PICKER) == null) { 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 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.ContentAlpha
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
@ -19,6 +20,7 @@ fun DisabledText(
style = MaterialTheme.typography.body1, style = MaterialTheme.typography.body1,
modifier = modifier modifier = modifier
.alpha(alpha = ContentAlpha.disabled) .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.clickable
import androidx.compose.foundation.layout.Row 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.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.unit.dp
@Composable @Composable
fun TaskEditRow( 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, content: @Composable () -> Unit,
onClick: (() -> Unit)? = null, 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.app.Activity
import android.content.Intent import android.content.Intent
import android.view.ViewGroup 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.ComposeView
import androidx.compose.ui.platform.LocalContext 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.google.android.material.composethemeadapter.MdcTheme
import com.todoroo.andlib.utility.DateUtilities import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.data.Task.Companion.hasDueTime import com.todoroo.astrid.data.Task.Companion.hasDueTime
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.tasks.R 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.collectAsStateLifecycleAware
import org.tasks.compose.edit.DueDateRow
import org.tasks.date.DateTimeUtils import org.tasks.date.DateTimeUtils
import org.tasks.dialogs.DateTimePicker import org.tasks.dialogs.DateTimePicker
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.ui.DeadlineControlSet.Companion.isOverdue
import java.time.format.FormatStyle import java.time.format.FormatStyle
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
@ -39,10 +28,21 @@ class DeadlineControlSet : TaskEditControlComposeFragment() {
(parent as ComposeView).apply { (parent as ComposeView).apply {
setContent { setContent {
MdcTheme { MdcTheme {
val dueDate = viewModel.dueDate.collectAsStateLifecycleAware().value
DueDateRow( DueDateRow(
viewModel = viewModel, dueDate = if (dueDate == 0L) {
locale = locale, null
displayFullDate = preferences.alwaysDisplayFullDate, } else {
DateUtilities.getRelativeDateTime(
LocalContext.current,
dueDate,
locale,
FormatStyle.FULL,
preferences.alwaysDisplayFullDate,
false
)
},
overdue = dueDate.isOverdue,
onClick = { onClick = {
val fragmentManager = parentFragmentManager val fragmentManager = parentFragmentManager
if (fragmentManager.findFragmentByTag(FRAG_TAG_DATE_PICKER) == null) { 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.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Parcelable import android.os.Parcelable
import androidx.compose.foundation.layout.Column import android.view.View
import androidx.compose.foundation.layout.Row import android.view.ViewGroup
import androidx.compose.foundation.layout.padding import androidx.compose.ui.platform.ComposeView
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 androidx.core.util.Pair import androidx.core.util.Pair
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState import com.google.accompanist.permissions.rememberMultiplePermissionsState
import com.google.android.material.composethemeadapter.MdcTheme
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.tasks.R import org.tasks.R
import org.tasks.Strings.isNullOrEmpty import org.tasks.Strings.isNullOrEmpty
import org.tasks.compose.DisabledText
import org.tasks.compose.collectAsStateLifecycleAware import org.tasks.compose.collectAsStateLifecycleAware
import org.tasks.compose.edit.LocationRow
import org.tasks.data.Geofence import org.tasks.data.Geofence
import org.tasks.data.Location import org.tasks.data.Location
import org.tasks.data.Place import org.tasks.data.Place
@ -35,8 +24,6 @@ import org.tasks.dialogs.GeofenceDialog
import org.tasks.extensions.Context.openUri import org.tasks.extensions.Context.openUri
import org.tasks.location.LocationPermissionDialog.Companion.newLocationPermissionDialog import org.tasks.location.LocationPermissionDialog.Companion.newLocationPermissionDialog
import org.tasks.location.LocationPickerActivity 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
import org.tasks.preferences.PermissionChecker.backgroundPermissions import org.tasks.preferences.PermissionChecker.backgroundPermissions
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
@ -46,8 +33,6 @@ import javax.inject.Inject
class LocationControlSet : TaskEditControlComposeFragment() { class LocationControlSet : TaskEditControlComposeFragment() {
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
@Inject lateinit var dialogBuilder: DialogBuilder @Inject lateinit var dialogBuilder: DialogBuilder
@Inject lateinit var device: Device
@Inject lateinit var permissionRequestor: FragmentPermissionRequestor
@Inject lateinit var permissionChecker: PermissionChecker @Inject lateinit var permissionChecker: PermissionChecker
private fun setLocation(location: Location?) { private fun setLocation(location: Location?) {
@ -94,40 +79,35 @@ class LocationControlSet : TaskEditControlComposeFragment() {
} }
@OptIn(ExperimentalPermissionsApi::class) @OptIn(ExperimentalPermissionsApi::class)
@Composable override fun bind(parent: ViewGroup?): View =
override fun Body() { (parent as ComposeView).apply {
val location = viewModel.selectedLocation.collectAsStateLifecycleAware().value setContent {
val hasPermissions = MdcTheme {
rememberMultiplePermissionsState(permissions = backgroundPermissions()) val hasPermissions =
.allPermissionsGranted rememberMultiplePermissionsState(permissions = backgroundPermissions())
if (location == null) { .allPermissionsGranted
DisabledText( LocationRow(
text = stringResource(id = R.string.add_location), location = viewModel.selectedLocation.collectAsStateLifecycleAware().value,
modifier = Modifier.padding(vertical = 20.dp) hasPermissions = hasPermissions,
) onClick = this@LocationControlSet::onRowClick,
} else { openGeofenceOptions = {
LocationRow( if (hasPermissions) {
name = location.displayName, showGeofenceOptions()
address = location.displayAddress, } else {
onClick = { newLocationPermissionDialog(
if (hasPermissions) { this@LocationControlSet,
showGeofenceOptions() REQUEST_LOCATION_PERMISSIONS
} else { )
newLocationPermissionDialog(this, REQUEST_LOCATION_PERMISSIONS) .show(parentFragmentManager, FRAG_TAG_REQUEST_LOCATION)
.show(parentFragmentManager, FRAG_TAG_REQUEST_LOCATION) }
} }
}, )
geofenceOn = hasPermissions && (location.isArrival || location.isDeparture) }
) }
} }
}
override val icon = R.drawable.ic_outline_place_24px
override fun controlId() = TAG override fun controlId() = TAG
override val isClickable = true
private fun openWebsite() { private fun openWebsite() {
viewModel.selectedLocation.value?.let { context?.openUri(it.url) } viewModel.selectedLocation.value?.let { context?.openUri(it.url) }
} }
@ -180,39 +160,3 @@ class LocationControlSet : TaskEditControlComposeFragment() {
private const val FRAG_TAG_REQUEST_LOCATION = "request_location" 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 package org.tasks.ui
import android.content.res.Configuration import android.view.View
import androidx.compose.foundation.layout.* import android.view.ViewGroup
import androidx.compose.material.MaterialTheme import androidx.compose.ui.platform.ComposeView
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.google.android.material.composethemeadapter.MdcTheme
import com.todoroo.astrid.data.Task
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.tasks.R import org.tasks.R
import org.tasks.compose.collectAsStateLifecycleAware import org.tasks.compose.collectAsStateLifecycleAware
import org.tasks.compose.edit.PriorityRow
@AndroidEntryPoint @AndroidEntryPoint
class PriorityControlSet : TaskEditControlComposeFragment() { class PriorityControlSet : TaskEditControlComposeFragment() {
@Composable override fun bind(parent: ViewGroup?): View =
override fun Body() { (parent as ComposeView).apply {
val priority = viewModel.priority.collectAsStateLifecycleAware() setContent {
PriorityRow( MdcTheme {
selected = priority.value, PriorityRow(
onClick = { viewModel.priority.value = it }) priority = viewModel.priority.collectAsStateLifecycleAware().value,
} onChangePriority = { viewModel.priority.value = it }
)
override val icon = R.drawable.ic_outline_flag_24px }
}
}
override fun controlId() = TAG override fun controlId() = TAG
@ -40,62 +30,3 @@ class PriorityControlSet : TaskEditControlComposeFragment() {
const val TAG = R.string.TEA_ctrl_importance_pref 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