Move more composable logic

pull/1952/head
Alex Baker 3 years ago
parent e7b6c96576
commit 00c80337de

@ -5,45 +5,33 @@
*/ */
package com.todoroo.astrid.files package com.todoroo.astrid.files
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import androidx.compose.foundation.clickable import android.view.View
import androidx.compose.foundation.layout.Column import android.view.ViewGroup
import androidx.compose.foundation.layout.Row import androidx.compose.ui.platform.ComposeView
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
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.Delete
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
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 kotlinx.coroutines.NonCancellable import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
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.AttachmentRow
import org.tasks.data.TaskAttachment import org.tasks.data.TaskAttachment
import org.tasks.data.TaskAttachmentDao import org.tasks.data.TaskAttachmentDao
import org.tasks.dialogs.AddAttachmentDialog import org.tasks.dialogs.AddAttachmentDialog
import org.tasks.dialogs.DialogBuilder import org.tasks.dialogs.DialogBuilder
import org.tasks.files.FileHelper import org.tasks.files.FileHelper
import org.tasks.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.ui.TaskEditControlComposeFragment import org.tasks.ui.TaskEditControlFragment
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class FilesControlSet : TaskEditControlComposeFragment() { class FilesControlSet : TaskEditControlFragment() {
@Inject lateinit var taskAttachmentDao: TaskAttachmentDao @Inject lateinit var taskAttachmentDao: TaskAttachmentDao
@Inject lateinit var dialogBuilder: DialogBuilder @Inject lateinit var dialogBuilder: DialogBuilder
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
@ -59,53 +47,25 @@ class FilesControlSet : TaskEditControlComposeFragment() {
} }
} }
private fun addAttachment() { override fun bind(parent: ViewGroup?): View =
AddAttachmentDialog.newAddAttachmentDialog(this) (parent as ComposeView).apply {
setContent {
MdcTheme {
AttachmentRow(
attachments = taskAttachmentDao.watchAttachments(viewModel.task.uuid)
.collectAsStateLifecycleAware(initial = emptyList()).value,
openAttachment = {
FileHelper.startActionView(requireActivity(), it.parseUri())
},
deleteAttachment = this@FilesControlSet::deleteAttachment,
addAttachment = {
AddAttachmentDialog.newAddAttachmentDialog(this@FilesControlSet)
.show(parentFragmentManager, FRAG_TAG_ADD_ATTACHMENT_DIALOG) .show(parentFragmentManager, FRAG_TAG_ADD_ATTACHMENT_DIALOG)
} },
@Composable
override fun Body() {
val attachments =
taskAttachmentDao.watchAttachments(viewModel.task.uuid)
.collectAsStateLifecycleAware(initial = emptyList()).value
Column(
modifier = Modifier.padding(top = if (attachments.isEmpty()) 0.dp else 8.dp),
) {
attachments.forEach {
Row(
modifier = Modifier
.clickable { showFile(it) },
verticalAlignment = CenterVertically,
) {
Text(
text = it.name!!,
modifier = Modifier.weight(1f),
)
IconButton(onClick = { deleteAttachment(it) }) {
Icon(
imageVector = Icons.Outlined.Delete,
contentDescription = stringResource(
id = R.string.delete
)
) )
} }
} }
} }
DisabledText(
text = stringResource(id = R.string.add_attachment),
modifier = Modifier
.fillMaxWidth()
.clickable { addAttachment() }
.padding(
top = if (attachments.isEmpty()) 20.dp else 8.dp,
bottom = 20.dp,
)
)
}
}
override val icon = R.drawable.ic_outline_attachment_24px
override fun controlId() = TAG override fun controlId() = TAG
@ -148,11 +108,6 @@ class FilesControlSet : TaskEditControlComposeFragment() {
.show() .show()
} }
@SuppressLint("NewApi")
private fun showFile(m: TaskAttachment) {
FileHelper.startActionView(requireActivity(), m.parseUri())
}
private fun copyToAttachmentDirectory(input: Uri?) { private fun copyToAttachmentDirectory(input: Uri?) {
newAttachment(FileHelper.copyToUri(requireContext(), preferences.attachmentsDirectory!!, input!!)) newAttachment(FileHelper.copyToUri(requireContext(), preferences.attachmentsDirectory!!, input!!))
} }

@ -24,11 +24,11 @@ import org.tasks.repeats.RecurrenceUtils.newRecur
import org.tasks.repeats.RepeatRuleToString import org.tasks.repeats.RepeatRuleToString
import org.tasks.time.DateTime import org.tasks.time.DateTime
import org.tasks.time.DateTimeUtils.currentTimeMillis import org.tasks.time.DateTimeUtils.currentTimeMillis
import org.tasks.ui.TaskEditControlComposeFragment import org.tasks.ui.TaskEditControlFragment
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class RepeatControlSet : TaskEditControlComposeFragment() { class RepeatControlSet : TaskEditControlFragment() {
@Inject lateinit var repeatRuleToString: RepeatRuleToString @Inject lateinit var repeatRuleToString: RepeatRuleToString
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {

@ -12,11 +12,11 @@ import org.tasks.compose.collectAsStateLifecycleAware
import org.tasks.compose.edit.TagsRow import org.tasks.compose.edit.TagsRow
import org.tasks.tags.TagPickerActivity import org.tasks.tags.TagPickerActivity
import org.tasks.ui.ChipProvider import org.tasks.ui.ChipProvider
import org.tasks.ui.TaskEditControlComposeFragment import org.tasks.ui.TaskEditControlFragment
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class TagsControlSet : TaskEditControlComposeFragment() { class TagsControlSet : TaskEditControlFragment() {
@Inject lateinit var chipProvider: ChipProvider @Inject lateinit var chipProvider: ChipProvider
override fun onRowClick() { override fun onRowClick() {

@ -7,38 +7,23 @@ package com.todoroo.astrid.timers
import android.app.Activity import android.app.Activity
import android.os.Bundle import android.os.Bundle
import android.text.format.DateUtils
import android.view.View import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.compose.foundation.layout.Row import androidx.compose.ui.platform.ComposeView
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.Pause
import androidx.compose.material.icons.outlined.PlayArrow
import androidx.compose.runtime.*
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.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.composethemeadapter.MdcTheme
import com.todoroo.astrid.data.Task import com.todoroo.astrid.data.Task
import com.todoroo.astrid.ui.TimeDurationControlSet import com.todoroo.astrid.ui.TimeDurationControlSet
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
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.TimerRow
import org.tasks.dialogs.DialogBuilder import org.tasks.dialogs.DialogBuilder
import org.tasks.themes.Theme import org.tasks.themes.Theme
import org.tasks.ui.TaskEditControlComposeFragment import org.tasks.ui.TaskEditControlFragment
import java.lang.System.currentTimeMillis
import javax.inject.Inject import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
/** /**
* Control Set for managing repeats * Control Set for managing repeats
@ -46,7 +31,7 @@ import kotlin.time.Duration.Companion.seconds
* @author Tim Su <tim></tim>@todoroo.com> * @author Tim Su <tim></tim>@todoroo.com>
*/ */
@AndroidEntryPoint @AndroidEntryPoint
class TimerControlSet : TaskEditControlComposeFragment() { class TimerControlSet : TaskEditControlFragment() {
@Inject lateinit var activity: Activity @Inject lateinit var activity: Activity
@Inject lateinit var dialogBuilder: DialogBuilder @Inject lateinit var dialogBuilder: DialogBuilder
@Inject lateinit var theme: Theme @Inject lateinit var theme: Theme
@ -103,76 +88,23 @@ class TimerControlSet : TaskEditControlComposeFragment() {
} }
} }
@Composable override fun bind(parent: ViewGroup?): View =
override fun Body() { (parent as ComposeView).apply {
var now by remember { mutableStateOf(currentTimeMillis()) } setContent {
val started = viewModel.timerStarted.collectAsStateLifecycleAware().value MdcTheme {
val newElapsed = if (started > 0) (now - started) / 1000L else 0 TimerRow(
val estimated = started = viewModel.timerStarted.collectAsStateLifecycleAware().value,
viewModel.estimatedSeconds.collectAsStateLifecycleAware().value.takeIf { it > 0 } estimated = viewModel.estimatedSeconds.collectAsStateLifecycleAware().value,
?.let { elapsed = viewModel.elapsedSeconds.collectAsStateLifecycleAware().value,
stringResource(id = R.string.TEA_timer_est, DateUtils.formatElapsedTime(it.toLong())) timerClicked = this@TimerControlSet::timerClicked,
} onClick = this@TimerControlSet::onRowClick,
val elapsed =
(newElapsed + viewModel.elapsedSeconds.collectAsStateLifecycleAware().value)
.takeIf { it > 0 }
?.let {
stringResource(id = R.string.TEA_timer_elap, DateUtils.formatElapsedTime(it))
}
val text = when {
estimated != null && elapsed != null -> "$estimated, $elapsed"
estimated != null -> estimated
elapsed != null -> elapsed
else -> null
}
Row {
if (text == null) {
DisabledText(
text = stringResource(id = R.string.TEA_timer_controls),
modifier = Modifier
.weight(1f)
.padding(vertical = 20.dp),
)
} else {
Text(
text = text,
modifier = Modifier
.weight(1f)
.padding(vertical = 20.dp),
)
}
IconButton(
onClick = {
now = currentTimeMillis()
timerClicked()
},
modifier = Modifier.padding(vertical = 8.dp),
) {
Icon(
imageVector = if (started > 0) {
Icons.Outlined.Pause
} else {
Icons.Outlined.PlayArrow
},
modifier = Modifier.alpha(ContentAlpha.medium),
contentDescription = null
) )
} }
} }
LaunchedEffect(key1 = started) {
while (started > 0) {
delay(1.seconds)
now = currentTimeMillis()
}
} }
}
override val icon = R.drawable.ic_outline_timer_24px
override fun controlId() = TAG override fun controlId() = TAG
override val isClickable = true
private fun timerActive() = viewModel.timerStarted.value > 0 private fun timerActive() = viewModel.timerStarted.value > 0
interface TimerControlSetCallback { interface TimerControlSetCallback {

@ -43,12 +43,12 @@ import org.tasks.date.DateTimeUtils
import org.tasks.dialogs.DialogBuilder import org.tasks.dialogs.DialogBuilder
import org.tasks.dialogs.MyTimePickerDialog import org.tasks.dialogs.MyTimePickerDialog
import org.tasks.reminders.AlarmToString import org.tasks.reminders.AlarmToString
import org.tasks.ui.TaskEditControlComposeFragment import org.tasks.ui.TaskEditControlFragment
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class ReminderControlSet : TaskEditControlComposeFragment() { class ReminderControlSet : TaskEditControlFragment() {
@Inject lateinit var activity: Activity @Inject lateinit var activity: Activity
@Inject lateinit var dialogBuilder: DialogBuilder @Inject lateinit var dialogBuilder: DialogBuilder
@Inject lateinit var alarmToString: AlarmToString @Inject lateinit var alarmToString: AlarmToString

@ -22,13 +22,13 @@ 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.preferences.Preferences import org.tasks.preferences.Preferences
import org.tasks.ui.TaskEditControlComposeFragment import org.tasks.ui.TaskEditControlFragment
import java.time.format.FormatStyle import java.time.format.FormatStyle
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class StartDateControlSet : TaskEditControlComposeFragment() { class StartDateControlSet : TaskEditControlFragment() {
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
@Inject lateinit var locale: Locale @Inject lateinit var locale: Locale

@ -0,0 +1,103 @@
package org.tasks.compose.edit
import android.content.res.Configuration
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
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.Delete
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
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.TaskAttachment
@Composable
fun AttachmentRow(
attachments: List<TaskAttachment>,
openAttachment: (TaskAttachment) -> Unit,
deleteAttachment: (TaskAttachment) -> Unit,
addAttachment: () -> Unit,
) {
TaskEditRow(
iconRes = R.drawable.ic_outline_attachment_24px,
content = {
Column(
modifier = Modifier.padding(top = if (attachments.isEmpty()) 0.dp else 8.dp),
) {
attachments.forEach {
Row(
modifier = Modifier
.clickable { openAttachment(it) },
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = it.name!!,
modifier = Modifier.weight(1f),
)
IconButton(onClick = { deleteAttachment(it) }) {
Icon(
imageVector = Icons.Outlined.Delete,
contentDescription = stringResource(
id = R.string.delete
)
)
}
}
}
DisabledText(
text = stringResource(id = R.string.add_attachment),
modifier = Modifier
.fillMaxWidth()
.clickable { addAttachment() }
.padding(
top = if (attachments.isEmpty()) 20.dp else 8.dp,
bottom = 20.dp,
)
)
}
},
)
}
@Preview(showBackground = true, widthDp = 320)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 320)
@Composable
fun NoAttachments() {
MdcTheme {
AttachmentRow(
attachments = emptyList(),
openAttachment = {},
deleteAttachment = {},
addAttachment = {},
)
}
}
@Preview(showBackground = true, widthDp = 320)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 320)
@Composable
fun AttachmentPreview() {
MdcTheme {
AttachmentRow(
attachments = listOf(
TaskAttachment("", "file://attachment.txt".toUri(), "attachment.txt")
),
openAttachment = {},
deleteAttachment = {},
addAttachment = {},
)
}
}

@ -0,0 +1,93 @@
package org.tasks.compose.edit
import android.content.res.Configuration
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.Delete
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
@Composable
fun CalendarRow(
eventUri: String?,
selectedCalendar: String?,
onClick: () -> Unit,
clear: () -> Unit,
) {
TaskEditRow(
iconRes = R.drawable.ic_outline_event_24px,
content = {
if (eventUri?.isNotBlank() == true) {
Row {
Text(
text = stringResource(id = R.string.gcal_TEA_showCalendar_label),
modifier = Modifier
.weight(1f)
.padding(vertical = 20.dp)
)
IconButton(
onClick = { clear() },
Modifier.padding(vertical = 8.dp),
) {
Icon(
imageVector = Icons.Outlined.Delete,
contentDescription = stringResource(id = R.string.delete),
modifier = Modifier.alpha(ContentAlpha.medium),
)
}
}
} else if (selectedCalendar?.isNotBlank() == true) {
Text(
text = selectedCalendar,
modifier = Modifier.padding(vertical = 20.dp),
)
} else {
DisabledText(
text = stringResource(id = R.string.dont_add_to_calendar),
modifier = Modifier.padding(vertical = 20.dp),
)
}
},
onClick = onClick
)
}
@Preview(showBackground = true, widthDp = 320)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 320)
@Composable
fun NoCalendar() {
MdcTheme {
CalendarRow(eventUri = null, selectedCalendar = null, onClick = {}, clear = {})
}
}
@Preview(showBackground = true, widthDp = 320)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 320)
@Composable
fun NewCalendar() {
MdcTheme {
CalendarRow(eventUri = null, selectedCalendar = "Personal", onClick = {}, clear = {})
}
}
@Preview(showBackground = true, widthDp = 320)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 320)
@Composable
fun ExistingCalendar() {
MdcTheme {
CalendarRow(eventUri = "abcd", selectedCalendar = null, onClick = {}, clear = {})
}
}

@ -0,0 +1,73 @@
package org.tasks.compose.edit
import android.content.res.Configuration
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
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.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.TaskEditRow
import java.text.SimpleDateFormat
import java.util.*
@Composable
fun InfoRow(
creationDate: Long?,
modificationDate: Long?,
completionDate: Long?,
locale: Locale = Locale.getDefault(),
) {
TaskEditRow(
iconRes = R.drawable.ic_outline_info_24px,
content = {
Column(modifier = Modifier.padding(vertical = 20.dp)) {
val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm", locale)
creationDate?.let {
Text(
text = stringResource(
id = R.string.sort_created_group,
formatter.format(it)
)
)
}
modificationDate?.let {
Text(
text = stringResource(
id = R.string.sort_modified_group,
formatter.format(it)
)
)
}
completionDate?.takeIf { it > 0 }?.let {
Text(
text = stringResource(
id = R.string.sort_completion_group,
formatter.format(it)
)
)
}
}
},
)
}
@ExperimentalComposeUiApi
@Preview(showBackground = true, widthDp = 320)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 320)
@Composable
fun InfoPreview() {
MdcTheme {
InfoRow(
creationDate = 1658727180000,
modificationDate = 1658813557000,
completionDate = 1658813557000,
locale = Locale.US,
)
}
}

@ -1,9 +1,14 @@
package org.tasks.compose.edit package org.tasks.compose.edit
import android.content.res.Configuration
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.google.android.material.composethemeadapter.MdcTheme
import com.todoroo.andlib.sql.QueryTemplate
import com.todoroo.astrid.api.Filter import com.todoroo.astrid.api.Filter
import org.tasks.R import org.tasks.R
import org.tasks.compose.ChipGroup import org.tasks.compose.ChipGroup
@ -35,3 +40,17 @@ fun ListRow(
onClick = onClick onClick = onClick
) )
} }
@ExperimentalComposeUiApi
@Preview(showBackground = true, widthDp = 320)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 320)
@Composable
fun ListPreview() {
MdcTheme {
ListRow(
list = Filter("Default list", QueryTemplate()),
colorProvider = { -769226 },
onClick = {},
)
}
}

@ -1,16 +1,20 @@
package org.tasks.compose.edit package org.tasks.compose.edit
import android.content.res.Configuration
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.google.android.material.composethemeadapter.MdcTheme
import org.tasks.R import org.tasks.R
import org.tasks.compose.Chip import org.tasks.compose.Chip
import org.tasks.compose.ChipGroup import org.tasks.compose.ChipGroup
import org.tasks.compose.DisabledText import org.tasks.compose.DisabledText
import org.tasks.compose.TaskEditRow import org.tasks.compose.TaskEditRow
import org.tasks.data.TagData import org.tasks.data.TagData
import org.tasks.themes.ColorProvider
import org.tasks.themes.CustomIcons import org.tasks.themes.CustomIcons
@Composable @Composable
@ -46,3 +50,69 @@ fun TagsRow(
onClick = onClick onClick = onClick
) )
} }
@Preview(showBackground = true, widthDp = 320)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 320)
@Composable
fun NoTags() {
MdcTheme {
TagsRow(
tags = emptyList(),
colorProvider = { 0 },
onClick = {},
)
}
}
@Preview(showBackground = true, widthDp = 320)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 320)
@Composable
fun SingleTag() {
MdcTheme {
TagsRow(
tags = listOf(
TagData("Home").apply {
setIcon(1062)
setColor(ColorProvider.BLUE_500)
}
),
colorProvider = { it },
onClick = {},
)
}
}
@Preview(showBackground = true, widthDp = 320)
@Composable
fun BunchOfTags() {
MdcTheme {
TagsRow(
tags = listOf(
TagData("One"),
TagData("Two"),
TagData("Three"),
TagData("Four"),
TagData("Five"),
),
colorProvider = { it },
onClick = {}
)
}
}
@Preview(showBackground = true, widthDp = 320)
@Composable
fun TagWithReallyLongName() {
MdcTheme {
TagsRow(
tags = listOf(
TagData("This is a tag with a really really long name").apply {
setIcon(1062)
setColor(ColorProvider.BLUE_500)
}
),
colorProvider = { it },
onClick = {},
)
}
}

@ -0,0 +1,125 @@
package org.tasks.compose.edit
import android.content.res.Configuration
import android.text.format.DateUtils
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.Pause
import androidx.compose.material.icons.outlined.PlayArrow
import androidx.compose.runtime.*
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 kotlinx.coroutines.delay
import org.tasks.R
import org.tasks.compose.DisabledText
import org.tasks.compose.TaskEditRow
import kotlin.time.Duration.Companion.seconds
@Composable
fun TimerRow(
started: Long,
estimated: Int,
elapsed: Int,
timerClicked: () -> Unit,
onClick: () -> Unit,
) {
TaskEditRow(
iconRes = R.drawable.ic_outline_timer_24px,
content = {
var now by remember { mutableStateOf(System.currentTimeMillis()) }
val newElapsed = if (started > 0) (now - started) / 1000L else 0
val estimatedString = estimated
.takeIf { it > 0 }
?.let {
stringResource(
id = R.string.TEA_timer_est,
DateUtils.formatElapsedTime(it.toLong())
)
}
val elapsedString =
(newElapsed + elapsed)
.takeIf { it > 0 }
?.let {
stringResource(
id = R.string.TEA_timer_elap,
DateUtils.formatElapsedTime(it)
)
}
val text = when {
estimatedString != null && elapsedString != null -> "$estimatedString, $elapsedString"
estimatedString != null -> estimatedString
elapsedString != null -> elapsedString
else -> null
}
Row {
if (text == null) {
DisabledText(
text = stringResource(id = R.string.TEA_timer_controls),
modifier = Modifier
.weight(1f)
.padding(vertical = 20.dp),
)
} else {
Text(
text = text,
modifier = Modifier
.weight(1f)
.padding(vertical = 20.dp),
)
}
IconButton(
onClick = {
now = System.currentTimeMillis()
timerClicked()
},
modifier = Modifier.padding(vertical = 8.dp),
) {
Icon(
imageVector = if (started > 0) {
Icons.Outlined.Pause
} else {
Icons.Outlined.PlayArrow
},
modifier = Modifier.alpha(ContentAlpha.medium),
contentDescription = null
)
}
}
LaunchedEffect(key1 = started) {
while (started > 0) {
delay(1.seconds)
now = System.currentTimeMillis()
}
}
},
onClick = onClick
)
}
@Preview(showBackground = true, widthDp = 320)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 320)
@Composable
fun NoTimer() {
MdcTheme {
TimerRow(started = 0, estimated = 0, elapsed = 0, timerClicked = {}, onClick = {})
}
}
@Preview(showBackground = true, widthDp = 320)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 320)
@Composable
fun RunningTimer() {
MdcTheme {
TimerRow(started = System.currentTimeMillis(), estimated = 900, elapsed = 400, timerClicked = {}, onClick = {})
}
}

@ -4,34 +4,25 @@ import android.app.Activity
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.provider.CalendarContract import android.provider.CalendarContract
import android.view.View
import android.view.ViewGroup
import android.widget.Toast.LENGTH_SHORT import android.widget.Toast.LENGTH_SHORT
import androidx.compose.foundation.layout.Row import androidx.compose.ui.platform.ComposeView
import androidx.compose.foundation.layout.padding import com.google.android.material.composethemeadapter.MdcTheme
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.Delete
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 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.calendars.CalendarPicker import org.tasks.calendars.CalendarPicker
import org.tasks.calendars.CalendarProvider import org.tasks.calendars.CalendarProvider
import org.tasks.compose.DisabledText
import org.tasks.compose.collectAsStateLifecycleAware import org.tasks.compose.collectAsStateLifecycleAware
import org.tasks.compose.edit.CalendarRow
import org.tasks.extensions.Context.toast import org.tasks.extensions.Context.toast
import org.tasks.preferences.PermissionChecker import org.tasks.preferences.PermissionChecker
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class CalendarControlSet : TaskEditControlComposeFragment() { class CalendarControlSet : TaskEditControlFragment() {
@Inject lateinit var activity: Activity @Inject lateinit var activity: Activity
@Inject lateinit var calendarProvider: CalendarProvider @Inject lateinit var calendarProvider: CalendarProvider
@Inject lateinit var permissionChecker: PermissionChecker @Inject lateinit var permissionChecker: PermissionChecker
@ -50,64 +41,33 @@ class CalendarControlSet : TaskEditControlComposeFragment() {
} }
} }
@Composable override fun bind(parent: ViewGroup?): View =
override fun Body() { (parent as ComposeView).apply {
val eventUri = viewModel.eventUri.collectAsStateLifecycleAware().value setContent {
val selectedCalendar = MdcTheme {
viewModel.selectedCalendar.collectAsStateLifecycleAware().value?.let { CalendarRow(
eventUri = viewModel.eventUri.collectAsStateLifecycleAware().value,
selectedCalendar = viewModel.selectedCalendar.collectAsStateLifecycleAware().value?.let {
calendarProvider.getCalendar(it)?.name calendarProvider.getCalendar(it)?.name
} },
if (eventUri?.isNotBlank() == true) { onClick = {
Row { if (viewModel.eventUri.value.isNullOrBlank()) {
Text( CalendarPicker.newCalendarPicker(this@CalendarControlSet, REQUEST_CODE_PICK_CALENDAR, calendarName)
text = stringResource(id = R.string.gcal_TEA_showCalendar_label), .show(parentFragmentManager, FRAG_TAG_CALENDAR_PICKER)
modifier = Modifier
.weight(1f)
.padding(vertical = 20.dp)
)
IconButton(
onClick = { clear() },
Modifier.padding(vertical = 8.dp),
) {
Icon(
imageVector = Icons.Outlined.Delete,
contentDescription = stringResource(id = R.string.delete),
modifier = Modifier.alpha(ContentAlpha.medium),
)
}
}
} else if (selectedCalendar?.isNotBlank() == true) {
Text(
text = selectedCalendar,
modifier = Modifier.padding(vertical = 20.dp),
)
} else { } else {
DisabledText( openCalendarEvent()
text = stringResource(id = R.string.dont_add_to_calendar),
modifier = Modifier.padding(vertical = 20.dp),
)
}
} }
},
override val icon = R.drawable.ic_outline_event_24px clear = {
override fun controlId() = TAG
override val isClickable = true
private fun clear() {
viewModel.selectedCalendar.value = null viewModel.selectedCalendar.value = null
viewModel.eventUri.value = null viewModel.eventUri.value = null
} }
)
override fun onRowClick() {
if (viewModel.eventUri.value.isNullOrBlank()) {
CalendarPicker.newCalendarPicker(this, REQUEST_CODE_PICK_CALENDAR, calendarName)
.show(parentFragmentManager, FRAG_TAG_CALENDAR_PICKER)
} else {
openCalendarEvent()
} }
} }
}
override fun controlId() = TAG
private fun openCalendarEvent() { private fun openCalendarEvent() {
val cr = activity.contentResolver val cr = activity.contentResolver

@ -1,46 +1,32 @@
package org.tasks.ui package org.tasks.ui
import androidx.compose.foundation.layout.Column import android.view.View
import androidx.compose.foundation.layout.padding import android.view.ViewGroup
import androidx.compose.material.Text import androidx.compose.ui.platform.ComposeView
import androidx.compose.runtime.Composable import com.google.android.material.composethemeadapter.MdcTheme
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import org.tasks.R import org.tasks.R
import org.tasks.preferences.Preferences import org.tasks.compose.edit.InfoRow
import java.text.SimpleDateFormat
import java.util.* import java.util.*
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class CreationDateControlSet : TaskEditControlComposeFragment() { class CreationDateControlSet : TaskEditControlFragment() {
@Inject lateinit var locale: Locale @Inject lateinit var locale: Locale
@Composable override fun bind(parent: ViewGroup?): View =
override fun Body() { (parent as ComposeView).apply {
Column(modifier = Modifier.padding(vertical = 20.dp)) { setContent {
val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm", locale) MdcTheme {
viewModel.creationDate?.let { InfoRow(
Text( creationDate = viewModel.creationDate,
text = stringResource(id = R.string.sort_created_group, formatter.format(it)) modificationDate = viewModel.modificationDate,
completionDate = viewModel.completionDate,
locale = locale,
) )
} }
viewModel.modificationDate?.let {
Text(
text = stringResource(id = R.string.sort_modified_group, formatter.format(it))
)
}
viewModel.completionDate?.takeIf { it > 0 }?.let {
Text(
text = stringResource(id = R.string.sort_completion_group, formatter.format(it))
)
} }
} }
}
override val icon = R.drawable.ic_outline_info_24px
override fun controlId() = TAG override fun controlId() = TAG

@ -20,7 +20,7 @@ import java.util.*
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class DeadlineControlSet : TaskEditControlComposeFragment() { class DeadlineControlSet : TaskEditControlFragment() {
@Inject lateinit var locale: Locale @Inject lateinit var locale: Locale
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences

@ -14,7 +14,7 @@ import org.tasks.ui.TaskEditViewModel.Companion.stripCarriageReturns
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class DescriptionControlSet : TaskEditControlComposeFragment() { class DescriptionControlSet : TaskEditControlFragment() {
@Inject lateinit var linkify: Linkify @Inject lateinit var linkify: Linkify
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
@Inject lateinit var markdownProvider: MarkdownProvider @Inject lateinit var markdownProvider: MarkdownProvider

@ -17,7 +17,7 @@ import org.tasks.compose.edit.ListRow
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class ListFragment : TaskEditControlComposeFragment() { class ListFragment : TaskEditControlFragment() {
@Inject lateinit var chipProvider: ChipProvider @Inject lateinit var chipProvider: ChipProvider
override fun bind(parent: ViewGroup?): View = override fun bind(parent: ViewGroup?): View =

@ -30,7 +30,7 @@ import org.tasks.preferences.Preferences
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class LocationControlSet : TaskEditControlComposeFragment() { class LocationControlSet : TaskEditControlFragment() {
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
@Inject lateinit var dialogBuilder: DialogBuilder @Inject lateinit var dialogBuilder: DialogBuilder
@Inject lateinit var permissionChecker: PermissionChecker @Inject lateinit var permissionChecker: PermissionChecker

@ -12,7 +12,7 @@ import org.tasks.preferences.Preferences
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class PriorityControlSet : TaskEditControlComposeFragment() { class PriorityControlSet : TaskEditControlFragment() {
@Inject lateinit var preferences: Preferences @Inject lateinit var preferences: Preferences
override fun bind(parent: ViewGroup?): View = override fun bind(parent: ViewGroup?): View =

@ -54,7 +54,7 @@ import org.tasks.themes.ColorProvider
import javax.inject.Inject import javax.inject.Inject
@AndroidEntryPoint @AndroidEntryPoint
class SubtaskControlSet : TaskEditControlComposeFragment() { class SubtaskControlSet : TaskEditControlFragment() {
@Inject lateinit var activity: Activity @Inject lateinit var activity: Activity
@Inject lateinit var taskCompleter: TaskCompleter @Inject lateinit var taskCompleter: TaskCompleter
@Inject lateinit var localBroadcastManager: LocalBroadcastManager @Inject lateinit var localBroadcastManager: LocalBroadcastManager

@ -1,61 +0,0 @@
package org.tasks.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
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.platform.ComposeView
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModelProvider
import com.google.android.material.composethemeadapter.MdcTheme
import org.tasks.compose.TaskEditIcon
import org.tasks.compose.TaskEditRow
abstract class TaskEditControlComposeFragment : TaskEditControlFragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val composeView = ComposeView(requireActivity())
viewModel = ViewModelProvider(requireParentFragment())[TaskEditViewModel::class.java]
bind(composeView)
createView(savedInstanceState)
return composeView
}
override fun bind(parent: ViewGroup?): View =
(parent as ComposeView).apply {
setContent {
MdcTheme {
TaskEditRow(
icon = { Icon() },
content = { Body() },
onClick = if (this@TaskEditControlComposeFragment.isClickable)
this@TaskEditControlComposeFragment::onRowClick
else
null
)
}
}
}
@Composable
protected open fun Icon() {
TaskEditIcon(
id = icon,
modifier = Modifier
.padding(start = 16.dp, top = 20.dp, end = 32.dp, bottom = 20.dp)
.alpha(ContentAlpha.medium),
)
}
@Composable
protected open fun Body() {}
}

@ -1,9 +1,21 @@
package org.tasks.ui package org.tasks.ui
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
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.platform.ComposeView
import androidx.compose.ui.unit.dp
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.google.android.material.composethemeadapter.MdcTheme
import org.tasks.compose.TaskEditIcon
import org.tasks.compose.TaskEditRow
abstract class TaskEditControlFragment : Fragment() { abstract class TaskEditControlFragment : Fragment() {
lateinit var viewModel: TaskEditViewModel lateinit var viewModel: TaskEditViewModel
@ -17,5 +29,45 @@ abstract class TaskEditControlFragment : Fragment() {
protected open val icon = 0 protected open val icon = 0
abstract fun controlId(): Int abstract fun controlId(): Int
protected abstract fun bind(parent: ViewGroup?): View
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val composeView = ComposeView(requireActivity())
viewModel = ViewModelProvider(requireParentFragment())[TaskEditViewModel::class.java]
bind(composeView)
createView(savedInstanceState)
return composeView
}
open fun bind(parent: ViewGroup?): View =
(parent as ComposeView).apply {
setContent {
MdcTheme {
TaskEditRow(
icon = { Icon() },
content = { Body() },
onClick = if (this@TaskEditControlFragment.isClickable)
this@TaskEditControlFragment::onRowClick
else
null
)
}
}
}
@Composable
protected open fun Icon() {
TaskEditIcon(
id = icon,
modifier = Modifier
.padding(start = 16.dp, top = 20.dp, end = 32.dp, bottom = 20.dp)
.alpha(ContentAlpha.medium),
)
}
@Composable
protected open fun Body() {}
} }
Loading…
Cancel
Save