Convert list settings to compose (#3163)

pull/3191/head
Hady 12 months ago committed by GitHub
parent 87c5ec9f14
commit 8e5b93fc10
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,20 @@
{
"version": 3,
"artifactType": {
"type": "APK",
"kind": "Directory"
},
"applicationId": "org.tasks.ak",
"variantName": "genericRelease",
"elements": [
{
"type": "SINGLE",
"filters": [],
"attributes": [],
"versionCode": 130804,
"versionName": "14.0.6",
"outputFile": "app-generic-release.apk"
}
],
"elementType": "File"
}

@ -2,6 +2,7 @@ package org.tasks.location
import android.annotation.SuppressLint
import android.content.Context
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import com.google.android.gms.maps.CameraUpdateFactory
import com.google.android.gms.maps.GoogleMap
@ -23,14 +24,18 @@ class GoogleMapFragment @Inject constructor(
private var map: GoogleMap? = null
private var circle: Circle? = null
override fun init(activity: AppCompatActivity, callback: MapFragmentCallback, dark: Boolean) {
override fun init(activity: AppCompatActivity, callback: MapFragmentCallback, dark: Boolean, parent: ViewGroup?) {
this.callback = callback
this.dark = dark
val fragmentManager = activity.supportFragmentManager
var mapFragment = fragmentManager.findFragmentByTag(FRAG_TAG_MAP) as SupportMapFragment?
if (mapFragment == null) {
mapFragment = SupportMapFragment()
if (parent == null) {
fragmentManager.beginTransaction().replace(R.id.map, mapFragment).commit()
} else {
fragmentManager.beginTransaction().add(parent, mapFragment, null).commit()
}
}
mapFragment.getMapAsync(this)
}

@ -1,100 +1,65 @@
package org.tasks.activities
import android.graphics.drawable.LayerDrawable
import android.os.Bundle
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.appcompat.widget.Toolbar
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.ui.Alignment
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.tasks.R
import org.tasks.compose.Constants
import org.tasks.compose.DeleteButton
import org.tasks.compose.IconPickerActivity.Companion.launchIconPicker
import org.tasks.compose.IconPickerActivity.Companion.registerForIconPickerResult
import org.tasks.compose.components.TasksIcon
import org.tasks.compose.ListSettings.ProgressBar
import org.tasks.compose.ListSettings.SettingsSurface
import org.tasks.compose.ListSettings.TitleInput
import org.tasks.compose.ListSettings.Toolbar
import org.tasks.compose.ListSettings.PromptAction
import org.tasks.compose.ListSettings.SelectColorRow
import org.tasks.compose.ListSettings.SelectIconRow
import org.tasks.dialogs.ColorPalettePicker
import org.tasks.dialogs.ColorPalettePicker.Companion.newColorPalette
import org.tasks.dialogs.ColorPickerAdapter.Palette
import org.tasks.dialogs.ColorWheelPicker
import org.tasks.dialogs.DialogBuilder
import org.tasks.extensions.addBackPressedCallback
import org.tasks.injection.ThemedInjectingAppCompatActivity
import org.tasks.themes.ColorProvider
import org.tasks.themes.DrawableUtil
import org.tasks.themes.TasksTheme
import org.tasks.themes.ThemeColor
import javax.inject.Inject
abstract class BaseListSettingsActivity : ThemedInjectingAppCompatActivity(), Toolbar.OnMenuItemClickListener, ColorPalettePicker.ColorPickedCallback, ColorWheelPicker.ColorPickedCallback {
@Inject lateinit var dialogBuilder: DialogBuilder
abstract class BaseListSettingsActivity : ThemedInjectingAppCompatActivity(), ColorPalettePicker.ColorPickedCallback, ColorWheelPicker.ColorPickedCallback {
@Inject lateinit var colorProvider: ColorProvider
protected abstract val defaultIcon: String
protected var selectedColor = 0
protected var selectedIcon = MutableStateFlow<String?>(null)
protected var selectedIcon = mutableStateOf<String?>(null)
private lateinit var clear: View
private lateinit var color: TextView
protected lateinit var toolbar: Toolbar
protected lateinit var colorRow: ViewGroup
protected val textState = mutableStateOf("")
protected val errorState = mutableStateOf("")
protected val colorState = mutableStateOf(Color.Unspecified)
protected val showProgress = mutableStateOf(false)
protected val promptDelete = mutableStateOf(false)
protected val promptDiscard = mutableStateOf(false)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val view = bind()
setContentView(view)
clear = findViewById<View>(R.id.clear).apply {
setOnClickListener { clearColor() }
}
color = findViewById(R.id.color)
colorRow = findViewById<ViewGroup>(R.id.color_row).apply {
setOnClickListener { showThemePicker() }
}
findViewById<ComposeView>(R.id.icon).setContent {
TasksTheme(theme = tasksTheme.themeBase.index) {
Row(
verticalAlignment = Alignment.CenterVertically,
) {
TasksIcon(
label = selectedIcon.collectAsStateWithLifecycle().value ?: defaultIcon
)
Spacer(modifier = Modifier.width(34.dp))
Text(
text = "Icon",
style = MaterialTheme.typography.bodyLarge.copy(
fontSize = 18.sp,
),
color = MaterialTheme.colorScheme.onSurface,
)
}
}
}
findViewById<View>(R.id.icon_row).setOnClickListener { showIconPicker() }
toolbar = view.findViewById(R.id.toolbar)
/* defaultIcon is initialized in the descendant's constructor so it can not be used
in constructor of the base class. So valid initial value for iconState is set here */
selectedIcon.value = defaultIcon
if (savedInstanceState != null) {
selectedColor = savedInstanceState.getInt(EXTRA_SELECTED_THEME)
selectedIcon.update { savedInstanceState.getString(EXTRA_SELECTED_ICON) }
}
toolbar.title = toolbarTitle
toolbar.navigationIcon = getDrawable(R.drawable.ic_outline_save_24px)
toolbar.setNavigationOnClickListener { lifecycleScope.launch { save() } }
if (!isNew) {
toolbar.inflateMenu(R.menu.menu_tag_settings)
selectedIcon.value = savedInstanceState.getString(EXTRA_SELECTED_ICON) ?: defaultIcon
}
toolbar.setOnMenuItemClickListener(this)
addBackPressedCallback {
discard()
}
@ -111,33 +76,25 @@ abstract class BaseListSettingsActivity : ThemedInjectingAppCompatActivity(), To
protected abstract val isNew: Boolean
protected abstract val toolbarTitle: String?
protected abstract suspend fun delete()
protected abstract fun bind(): View
protected open fun discard() {
if (!hasChanges()) {
finish()
} else {
dialogBuilder
.newDialog(R.string.discard_changes)
.setPositiveButton(R.string.discard) { _, _ -> finish() }
.setNegativeButton(R.string.cancel, null)
.show()
}
if (hasChanges()) promptDiscard.value = true
else finish()
}
private fun clearColor() {
protected fun clearColor() {
onColorPicked(0)
}
private fun showThemePicker() {
protected fun showThemePicker() {
newColorPalette(null, 0, selectedColor, Palette.COLORS)
.show(supportFragmentManager, FRAG_TAG_COLOR_PICKER)
}
val launcher = registerForIconPickerResult { selected ->
selectedIcon.update { selected }
selectedIcon.value = selected
}
private fun showIconPicker() {
fun showIconPicker() {
launcher.launchIconPicker(this, selectedIcon.value)
}
@ -146,38 +103,64 @@ abstract class BaseListSettingsActivity : ThemedInjectingAppCompatActivity(), To
updateTheme()
}
override fun onMenuItemClick(item: MenuItem): Boolean {
if (item.itemId == R.id.delete) {
promptDelete()
return true
}
return onOptionsItemSelected(item)
}
protected open fun promptDelete() { promptDelete.value = true }
protected fun updateTheme() {
val themeColor: ThemeColor =
if (selectedColor == 0) this.themeColor
else colorProvider.getThemeColor(selectedColor, true)
protected open fun promptDelete() {
dialogBuilder
.newDialog(R.string.delete_tag_confirmation, toolbarTitle)
.setPositiveButton(R.string.delete) { _, _ -> lifecycleScope.launch { delete() } }
.setNegativeButton(R.string.cancel, null)
.show()
colorState.value =
if (selectedColor == 0) Color.Unspecified
else Color((colorProvider.getThemeColor(selectedColor, true)).primaryColor)
//iconState.intValue = (getIconResId(selectedIcon) ?: getIconResId(defaultIcon))!!
themeColor.applyToNavigationBar(this)
}
protected fun updateTheme() {
val themeColor: ThemeColor
if (selectedColor == 0) {
themeColor = this.themeColor
DrawableUtil.setLeftDrawable(this, color, R.drawable.ic_outline_not_interested_24px)
DrawableUtil.getLeftDrawable(color).setTint(getColor(R.color.icon_tint_with_alpha))
clear.visibility = View.GONE
} else {
themeColor = colorProvider.getThemeColor(selectedColor, true)
DrawableUtil.setLeftDrawable(this, color, R.drawable.color_picker)
val leftDrawable = DrawableUtil.getLeftDrawable(color)
(if (leftDrawable is LayerDrawable) leftDrawable.getDrawable(0) else leftDrawable)
.setTint(themeColor.primaryColor)
clear.visibility = View.VISIBLE
/** Standard @Compose view content for descendants. Caller must wrap it to TasksTheme{} */
@Composable
protected fun baseSettingsContent(
title: String = toolbarTitle ?: "",
requestKeyboard: Boolean = isNew,
optionButton: @Composable () -> Unit = { if (!isNew) DeleteButton { promptDelete() } },
extensionContent: @Composable ColumnScope.() -> Unit = {}
) {
SettingsSurface {
Toolbar(
title = title,
save = { lifecycleScope.launch { save() } },
optionButton = optionButton
)
ProgressBar(showProgress)
TitleInput(
text = textState, error = errorState, requestKeyboard = requestKeyboard,
modifier = Modifier.padding(horizontal = Constants.KEYLINE_FIRST)
)
Column(modifier = Modifier.fillMaxWidth()) {
SelectColorRow(
color = colorState,
selectColor = { showThemePicker() },
clearColor = { clearColor() })
SelectIconRow(
icon = selectedIcon.value ?: defaultIcon,
selectIcon = { showIconPicker() })
extensionContent()
PromptAction(
showDialog = promptDelete,
title = stringResource(id = R.string.delete_tag_confirmation, title),
onAction = { lifecycleScope.launch { delete() } }
)
PromptAction(
showDialog = promptDiscard,
title = stringResource(id = R.string.discard_changes),
onAction = { lifecycleScope.launch { finish() } }
)
}
}
themeColor.applyToNavigationBar(this)
}
companion object {

@ -1,56 +1,60 @@
package org.tasks.activities
import android.app.Activity
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.view.MenuItem
import android.widget.EditText
import android.widget.FrameLayout
import androidx.core.widget.addTextChangedListener
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Help
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.button.MaterialButtonToggleGroup
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import org.tasks.data.sql.Field
import org.tasks.data.sql.Query
import org.tasks.data.sql.UnaryCriterion
import com.todoroo.astrid.activity.MainActivity
import com.todoroo.astrid.activity.TaskListFragment
import com.todoroo.astrid.api.BooleanCriterion
import org.tasks.filters.CustomFilter
import com.todoroo.astrid.api.CustomFilterCriterion
import com.todoroo.astrid.api.MultipleSelectCriterion
import com.todoroo.astrid.api.PermaSql
import com.todoroo.astrid.api.TextInputCriterion
import com.todoroo.astrid.core.CriterionInstance
import com.todoroo.astrid.core.CustomFilterAdapter
import com.todoroo.astrid.core.CustomFilterItemTouchHelper
import org.tasks.data.db.Database
import org.tasks.data.entity.Task
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.Strings
import org.tasks.data.entity.Filter
import org.tasks.data.dao.FilterDao
import org.tasks.compose.DeleteButton
import org.tasks.compose.FilterCondition.FilterCondition
import org.tasks.compose.FilterCondition.InputTextOption
import org.tasks.compose.FilterCondition.NewCriterionFAB
import org.tasks.compose.FilterCondition.SelectCriterionType
import org.tasks.compose.FilterCondition.SelectFromList
import org.tasks.data.NO_ORDER
import org.tasks.data.dao.FilterDao
import org.tasks.data.dao.TaskDao.TaskCriteria.activeAndVisible
import org.tasks.data.db.Database
import org.tasks.data.entity.Filter
import org.tasks.data.entity.Task
import org.tasks.data.rawQuery
import org.tasks.databinding.FilterSettingsActivityBinding
import org.tasks.data.sql.Field
import org.tasks.data.sql.Query
import org.tasks.data.sql.UnaryCriterion
import org.tasks.db.QueryUtils
import org.tasks.extensions.Context.hideKeyboard
import org.tasks.extensions.Context.openUri
import org.tasks.extensions.hideKeyboard
import org.tasks.filters.CustomFilter
import org.tasks.filters.FilterCriteriaProvider
import org.tasks.filters.mapToSerializedString
import org.tasks.themes.TasksIcons
import org.tasks.themes.TasksTheme
import java.util.Locale
import javax.inject.Inject
import kotlin.math.max
@ -63,23 +67,23 @@ class FilterSettingsActivity : BaseListSettingsActivity() {
@Inject lateinit var filterCriteriaProvider: FilterCriteriaProvider
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
private lateinit var name: TextInputEditText
private lateinit var nameLayout: TextInputLayout
private lateinit var recyclerView: RecyclerView
private lateinit var fab: ExtendedFloatingActionButton
private var filter: CustomFilter? = null
private lateinit var adapter: CustomFilterAdapter
private var criteria: MutableList<CriterionInstance> = ArrayList()
override val defaultIcon = TasksIcons.FILTER_LIST
private var criteria: SnapshotStateList<CriterionInstance> = emptyList<CriterionInstance>().toMutableStateList()
private val fabExtended = mutableStateOf(false)
private val editCriterionType: MutableState<String?> = mutableStateOf(null)
private val newCriterionTypes: MutableState<List<CustomFilterCriterion>?> = mutableStateOf(null)
private val newCriterionOptions: MutableState<CriterionInstance?> = mutableStateOf(null)
override fun onCreate(savedInstanceState: Bundle?) {
filter = intent.getParcelableExtra(TOKEN_FILTER)
super.onCreate(savedInstanceState)
if (savedInstanceState == null && filter != null) {
selectedColor = filter!!.tint
selectedIcon.update { filter!!.icon }
name.setText(filter!!.title)
selectedIcon.value = filter!!.icon ?: defaultIcon
textState.value = filter!!.title ?: ""
}
when {
savedInstanceState != null -> lifecycleScope.launch {
@ -93,36 +97,33 @@ class FilterSettingsActivity : BaseListSettingsActivity() {
setCriteria(filterCriteriaProvider.fromString(filter!!.criterion))
}
intent.hasExtra(EXTRA_CRITERIA) -> lifecycleScope.launch {
name.setText(intent.getStringExtra(EXTRA_TITLE))
textState.value = intent.getStringExtra(EXTRA_TITLE) ?: ""
setCriteria(
filterCriteriaProvider.fromString(intent.getStringExtra(EXTRA_CRITERIA)!!)
)
}
else -> setCriteria(universe())
}
recyclerView.layoutManager = LinearLayoutManager(this)
ItemTouchHelper(
CustomFilterItemTouchHelper(this, this::onMove, this::onDelete, this::updateList))
.attachToRecyclerView(recyclerView)
if (isNew) {
toolbar.inflateMenu(R.menu.menu_help)
}
updateTheme()
}
} /* end onCreate */
private fun universe() = listOf(CriterionInstance().apply {
criterion = filterCriteriaProvider.startingUniverse
type = CriterionInstance.TYPE_UNIVERSE
})
private fun setCriteria(criteria: List<CriterionInstance>) {
this.criteria = criteria
private fun setCriteria(criteriaList: List<CriterionInstance>) {
criteria = criteriaList
.ifEmpty { universe() }
.toMutableList()
adapter = CustomFilterAdapter(criteria, locale) { replaceId: String -> onClick(replaceId) }
recyclerView.adapter = adapter
fab.isExtended = isNew || adapter.itemCount <= 1
.toMutableStateList()
fabExtended.value = isNew || criteria.size <= 1
updateList()
this.setContent {
TasksTheme { ActivityContent() }
}
}
private fun onDelete(index: Int) {
@ -133,95 +134,13 @@ class FilterSettingsActivity : BaseListSettingsActivity() {
private fun onMove(from: Int, to: Int) {
val criterion = criteria.removeAt(from)
criteria.add(to, criterion)
adapter.notifyItemMoved(from, to)
}
private fun onClick(replaceId: String) {
val criterionInstance = criteria.find { it.id == replaceId }!!
val view = layoutInflater.inflate(R.layout.dialog_custom_filter_row_edit, recyclerView, false)
val group: MaterialButtonToggleGroup = view.findViewById(R.id.button_toggle)
val selected = getSelected(criterionInstance)
group.check(selected)
dialogBuilder
.newDialog(criterionInstance.titleFromCriterion)
.setView(view)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.ok) { _, _ ->
criterionInstance.type = getType(group.checkedButtonId)
updateList()
}
.setNeutralButton(R.string.help) { _, _ -> help() }
.show()
}
private fun getSelected(instance: CriterionInstance): Int =
when (instance.type) {
CriterionInstance.TYPE_ADD -> R.id.button_or
CriterionInstance.TYPE_SUBTRACT -> R.id.button_not
else -> R.id.button_and
}
private fun getType(selected: Int): Int =
when (selected) {
R.id.button_or -> CriterionInstance.TYPE_ADD
R.id.button_not -> CriterionInstance.TYPE_SUBTRACT
else -> CriterionInstance.TYPE_INTERSECT
}
private fun addCriteria() {
hideKeyboard()
fab.shrink()
private fun newCriterion() {
fabExtended.value = false // a.k.a. fab.shrink()
lifecycleScope.launch {
val all = filterCriteriaProvider.all()
val names = all.map(CustomFilterCriterion::getName)
dialogBuilder.newDialog()
.setItems(names) { dialog: DialogInterface, which: Int ->
val instance = CriterionInstance()
instance.criterion = all[which]
showOptionsFor(instance) {
criteria.add(instance)
updateList()
}
dialog.dismiss()
}
.show()
}
}
/** Show options menu for the given criterioninstance */
private fun showOptionsFor(item: CriterionInstance, onComplete: Runnable?) {
if (item.criterion is BooleanCriterion) {
onComplete?.run()
return
newCriterionTypes.value = filterCriteriaProvider.all()
}
val dialog = dialogBuilder.newDialog(item.criterion.name)
if (item.criterion is MultipleSelectCriterion) {
val multiSelectCriterion = item.criterion as MultipleSelectCriterion
val titles = multiSelectCriterion.entryTitles
val listener = DialogInterface.OnClickListener { _: DialogInterface?, which: Int ->
item.selectedIndex = which
onComplete?.run()
}
dialog.setItems(titles, listener)
} else if (item.criterion is TextInputCriterion) {
val textInCriterion = item.criterion as TextInputCriterion
val frameLayout = FrameLayout(this)
frameLayout.setPadding(10, 0, 10, 0)
val editText = EditText(this)
editText.setText(item.selectedText)
editText.hint = textInCriterion.hint
frameLayout.addView(
editText,
FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT))
dialog
.setView(frameLayout)
.setPositiveButton(R.string.ok) { _, _ ->
item.selectedText = editText.text.toString()
onComplete?.run()
}
}
dialog.show()
}
override fun onSaveInstanceState(outState: Bundle) {
@ -232,15 +151,16 @@ class FilterSettingsActivity : BaseListSettingsActivity() {
override val isNew: Boolean
get() = filter == null
override val toolbarTitle: String?
get() = if (isNew) getString(R.string.FLA_new_filter) else filter!!.title
override val toolbarTitle: String
get() = if (isNew) getString(R.string.FLA_new_filter) else filter?.title ?: ""
override suspend fun save() {
val newName = newName
if (Strings.isNullOrEmpty(newName)) {
nameLayout.error = getString(R.string.name_cannot_be_empty)
errorState.value = getString(R.string.name_cannot_be_empty)
return
}
if (hasChanges()) {
var f = Filter(
id = filter?.id ?: 0L,
@ -272,7 +192,7 @@ class FilterSettingsActivity : BaseListSettingsActivity() {
}
private val newName: String
get() = name.text.toString().trim { it <= ' ' }
get() = textState.value.trim { it <= ' ' }
override fun hasChanges(): Boolean {
return if (isNew) {
@ -287,24 +207,9 @@ class FilterSettingsActivity : BaseListSettingsActivity() {
}
override fun finish() {
hideKeyboard(name)
super.finish()
}
override fun bind() = FilterSettingsActivityBinding.inflate(layoutInflater).let {
name = it.name.apply {
addTextChangedListener(
onTextChanged = { _, _, _, _ -> nameLayout.error = null }
)
}
nameLayout = it.nameLayout
recyclerView = it.recyclerView
fab = it.fab.apply {
setOnClickListener { addCriteria() }
}
it.root
}
override suspend fun delete() {
filterDao.delete(filter!!.id)
setResult(
@ -312,21 +217,15 @@ class FilterSettingsActivity : BaseListSettingsActivity() {
finish()
}
override fun onMenuItemClick(item: MenuItem): Boolean =
if (item.itemId == R.id.menu_help) {
help()
true
} else {
super.onMenuItemClick(item)
}
private fun help() = openUri(R.string.url_filters)
private fun updateList() = lifecycleScope.launch {
val newList = emptyList<CriterionInstance>().toMutableList()
var max = 0
var last = -1
val sql = StringBuilder(Query.select(Field.COUNT).from(Task.TABLE).toString())
.append(" WHERE ")
for (instance in criteria) {
when (instance.type) {
CriterionInstance.TYPE_ADD -> sql.append("OR ")
@ -353,12 +252,138 @@ class FilterSettingsActivity : BaseListSettingsActivity() {
last = instance.end
max = max(max, last)
}
newList.add(instance)
}
for (instance in criteria) {
for (instance in newList) {
instance.max = max
}
adapter.submitList(criteria)
criteria.clear()
criteria.addAll(newList)
}
@Composable
private fun ActivityContent ()
{
TasksTheme {
Box( // to layout FAB over the main content
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.TopStart
) {
baseSettingsContent(
optionButton = {
if (isNew) {
IconButton(onClick = { help() }) {
Icon(imageVector = Icons.Outlined.Help, contentDescription = "")
}
} else DeleteButton{ promptDelete() }
}
) {
FilterCondition(
items = criteria,
onDelete = { index -> onDelete(index) },
doSwap = { from, to -> onMove(from, to) },
onComplete = { updateList() },
onClick = { id -> editCriterionType.value = id }
)
}
NewCriterionFAB(fabExtended) { newCriterion() }
/** edit given criterion type (AND|OR|NOT) **/
editCriterionType.value?.let { itemId ->
val index = criteria.indexOfFirst { it.id == itemId }
assert(index >= 0)
val criterionInstance = criteria[index]
if (criterionInstance.type != CriterionInstance.TYPE_UNIVERSE) {
SelectCriterionType(
title = criterionInstance.titleFromCriterion,
selected = when (criterionInstance.type) {
CriterionInstance.TYPE_INTERSECT -> 0
CriterionInstance.TYPE_ADD -> 1
else -> 2
},
types = listOf(
stringResource(R.string.custom_filter_and),
stringResource(R.string.custom_filter_or),
stringResource(R.string.custom_filter_not)
),
help = { help() },
onCancel = { editCriterionType.value = null }
) { selected ->
val type = when (selected) {
0 -> CriterionInstance.TYPE_INTERSECT
1 -> CriterionInstance.TYPE_ADD
else -> CriterionInstance.TYPE_SUBTRACT
}
if (criterionInstance.type != type) {
criterionInstance.type = type
updateList()
}
editCriterionType.value = null
}
}
} /* end (AND|OR|NOT) dialog */
/** dialog to select new criterion category **/
newCriterionTypes.value?.let { list ->
SelectFromList(
names = list.map(CustomFilterCriterion::getName),
onCancel = { newCriterionTypes.value = null },
onSelected = { which ->
val instance = CriterionInstance()
instance.criterion = list[which]
newCriterionTypes.value = null
if (instance.criterion is BooleanCriterion) {
criteria.add(instance)
updateList()
} else
newCriterionOptions.value = instance
}
)
} /* end dialog */
/** Show options menu for the given CriterionInstance */
newCriterionOptions.value?.let { instance ->
when (instance.criterion) {
is MultipleSelectCriterion -> {
val multiSelectCriterion = instance.criterion as MultipleSelectCriterion
val list = multiSelectCriterion.entryTitles.toList()
SelectFromList(
names = list,
title = instance.criterion.name,
onCancel = { newCriterionOptions.value = null },
onSelected = { which ->
instance.selectedIndex = which
criteria.add(instance)
updateList()
newCriterionOptions.value = null
}
)
}
is TextInputCriterion -> {
val textInCriterion = instance.criterion as TextInputCriterion
InputTextOption (
title = textInCriterion.name,
onCancel = { newCriterionOptions.value = null },
onDone = { text ->
text.trim().takeIf{ it != "" }?. let { text ->
instance.selectedText = text
criteria.add(instance)
updateList()
}
newCriterionOptions.value = null
}
)
}
else -> assert(false) { "Unexpected Criterion type" }
}
} /* end given criteria options dialog */
}
}
} /* activityContent */
companion object {
const val TOKEN_FILTER = "token_filter"

@ -1,35 +1,30 @@
package org.tasks.activities
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.ProgressBar
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.material3.SnackbarHostState
import androidx.lifecycle.lifecycleScope
import com.google.android.material.textfield.TextInputEditText
import com.google.api.services.tasks.model.TaskList
import com.todoroo.astrid.activity.MainActivity
import com.todoroo.astrid.activity.TaskListFragment
import com.todoroo.astrid.service.TaskDeleter
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.compose.ListSettings.Toaster
import org.tasks.data.dao.GoogleTaskListDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar
import org.tasks.databinding.ActivityGoogleTaskListSettingsBinding
import org.tasks.extensions.Context.hideKeyboard
import org.tasks.extensions.Context.toast
import org.tasks.filters.GtasksFilter
import org.tasks.themes.TasksIcons
import org.tasks.themes.TasksTheme
import timber.log.Timber
import javax.inject.Inject
@ -39,9 +34,6 @@ class GoogleTaskListSettingsActivity : BaseListSettingsActivity() {
@Inject lateinit var taskDeleter: TaskDeleter
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
private lateinit var name: TextInputEditText
private lateinit var progressView: ProgressBar
private var isNewList = false
private lateinit var gtasksList: CaldavCalendar
private val createListViewModel: CreateListViewModel by viewModels()
@ -49,6 +41,8 @@ class GoogleTaskListSettingsActivity : BaseListSettingsActivity() {
private val deleteListViewModel: DeleteListViewModel by viewModels()
override val defaultIcon = TasksIcons.LIST
val snackbar = SnackbarHostState()
override fun onCreate(savedInstanceState: Bundle?) {
gtasksList = intent.getParcelableExtra(EXTRA_STORE_DATA)
?: CaldavCalendar(
@ -59,23 +53,28 @@ class GoogleTaskListSettingsActivity : BaseListSettingsActivity() {
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
selectedColor = gtasksList.color
selectedIcon.update { gtasksList.icon }
}
if (isNewList) {
name.requestFocus()
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(name, InputMethodManager.SHOW_IMPLICIT)
} else {
name.setText(gtasksList.name)
selectedIcon.value = gtasksList.icon ?: defaultIcon
}
if (!isNewList) textState.value = gtasksList.name!!
if (createListViewModel.inProgress
|| renameListViewModel.inProgress
|| deleteListViewModel.inProgress) {
showProgressIndicator()
}
createListViewModel.observe(this, this::onListCreated, this::requestFailed)
renameListViewModel.observe(this, this::onListRenamed, this::requestFailed)
deleteListViewModel.observe(this, this::onListDeleted, this::requestFailed)
setContent {
TasksTheme {
baseSettingsContent()
Toaster(state = snackbar)
}
}
updateTheme()
}
@ -86,14 +85,14 @@ class GoogleTaskListSettingsActivity : BaseListSettingsActivity() {
get() = if (isNew) getString(R.string.new_list) else gtasksList.name!!
private fun showProgressIndicator() {
progressView.visibility = View.VISIBLE
showProgress.value = true
}
private fun hideProgressIndicator() {
progressView.visibility = View.GONE
showProgress.value = false
}
private fun requestInProgress() = progressView.visibility == View.VISIBLE
private fun requestInProgress() = showProgress.value
override suspend fun save() {
if (requestInProgress()) {
@ -101,7 +100,7 @@ class GoogleTaskListSettingsActivity : BaseListSettingsActivity() {
}
val newName = newName
if (isNullOrEmpty(newName)) {
toast(R.string.name_cannot_be_empty)
errorState.value = getString(R.string.name_cannot_be_empty)
return
}
when {
@ -133,16 +132,9 @@ class GoogleTaskListSettingsActivity : BaseListSettingsActivity() {
}
override fun finish() {
hideKeyboard(name)
super.finish()
}
override fun bind() = ActivityGoogleTaskListSettingsBinding.inflate(layoutInflater).let {
name = it.name
progressView = it.progressBar.progressBar
it.root
}
override fun promptDelete() {
if (!requestInProgress()) {
super.promptDelete()
@ -161,7 +153,7 @@ class GoogleTaskListSettingsActivity : BaseListSettingsActivity() {
}
private val newName: String
get() = name.text.toString().trim { it <= ' ' }
get() = textState.value.trim { it <= ' ' }
override fun hasChanges(): Boolean =
if (isNewList) {
@ -219,7 +211,8 @@ class GoogleTaskListSettingsActivity : BaseListSettingsActivity() {
private fun requestFailed(error: Throwable) {
Timber.e(error)
hideProgressIndicator()
toast(R.string.gtasks_GLA_errorIOAuth)
lifecycleScope.launch { snackbar.showSnackbar(getString(R.string.gtasks_GLA_errorIOAuth)) }
//toast(R.string.gtasks_GLA_errorIOAuth)
return
}

@ -3,46 +3,57 @@ package org.tasks.activities
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.core.widget.addTextChangedListener
import com.google.android.material.slider.Slider
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.LinearLayout
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.todoroo.astrid.activity.MainActivity
import com.todoroo.astrid.activity.TaskListFragment
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.update
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.compose.Constants
import org.tasks.data.dao.LocationDao
import org.tasks.data.displayName
import org.tasks.data.entity.Place
import org.tasks.data.mapPosition
import org.tasks.databinding.ActivityLocationSettingsBinding
import org.tasks.extensions.formatNumber
import org.tasks.filters.PlaceFilter
import org.tasks.location.MapFragment
import org.tasks.preferences.Preferences
import org.tasks.themes.TasksIcons
import org.tasks.themes.TasksTheme
import java.util.Locale
import javax.inject.Inject
import kotlin.math.roundToInt
@AndroidEntryPoint
class PlaceSettingsActivity : BaseListSettingsActivity(), MapFragment.MapFragmentCallback,
Slider.OnChangeListener {
class PlaceSettingsActivity : BaseListSettingsActivity(),
MapFragment.MapFragmentCallback {
companion object {
const val EXTRA_PLACE = "extra_place"
private const val MIN_RADIUS = 75
private const val MAX_RADIUS = 1000
private const val STEP = 25.0
private const val STEP = 25
}
private lateinit var name: TextInputEditText
private lateinit var nameLayout: TextInputLayout
private lateinit var slider: Slider
@Inject lateinit var locationDao: LocationDao
@Inject lateinit var map: MapFragment
@Inject lateinit var preferences: Preferences
@ -52,6 +63,9 @@ class PlaceSettingsActivity : BaseListSettingsActivity(), MapFragment.MapFragmen
private lateinit var place: Place
override val defaultIcon = TasksIcons.PLACE
private val sliderPos = mutableFloatStateOf(100f)
private lateinit var viewHolder: ViewGroup
override fun onCreate(savedInstanceState: Bundle?) {
if (intent?.hasExtra(EXTRA_PLACE) != true) {
finish()
@ -68,52 +82,89 @@ class PlaceSettingsActivity : BaseListSettingsActivity(), MapFragment.MapFragmen
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
name.setText(place.displayName)
textState.value = place.displayName
selectedColor = place.color
selectedIcon.update { place.icon }
selectedIcon.value = place.icon ?: defaultIcon
}
sliderPos.floatValue = (place.radius / STEP * STEP).toFloat()
val dark = preferences.mapTheme == 2
|| preferences.mapTheme == 0 && tasksTheme.themeBase.isDarkTheme(this)
map.init(this, this, dark)
updateTheme()
}
override fun bind() = ActivityLocationSettingsBinding.inflate(layoutInflater).let {
name = it.name.apply {
addTextChangedListener(
onTextChanged = { _, _, _, _ -> nameLayout.error = null }
)
}
nameLayout = it.nameLayout
slider = it.slider.apply {
setLabelFormatter { value ->
getString(
setContent {
TasksTheme {
baseSettingsContent {
Row(
modifier = Modifier
.requiredHeight(56.dp)
.fillMaxWidth()
.padding(horizontal = Constants.KEYLINE_FIRST),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween
) {
Text(stringResource(id = R.string.geofence_radius))
Row(horizontalArrangement = Arrangement.End) {
Text(getString(
R.string.location_radius_meters,
locale.formatNumber(value.toInt())
locale.formatNumber(sliderPos.floatValue.roundToInt()
)))
}
}
Slider(
modifier = Modifier.fillMaxWidth(),
value = sliderPos.floatValue,
valueRange = (MIN_RADIUS.toFloat()..MAX_RADIUS.toFloat()),
steps = (MAX_RADIUS - MIN_RADIUS) / STEP - 1,
onValueChange = { sliderPos.floatValue = it; updateGeofenceCircle(sliderPos.floatValue) },
colors = SliderDefaults.colors(
thumbColor = MaterialTheme.colorScheme.secondary,
activeTrackColor = MaterialTheme.colorScheme.secondary,
inactiveTrackColor = colorResource(id = R.color.text_tertiary),
activeTickColor = MaterialTheme.colorScheme.secondary,
inactiveTickColor = colorResource(id = R.color.text_tertiary),
disabledActiveTrackColor = MaterialTheme.colorScheme.secondary,
disabledInactiveTrackColor = colorResource(id = R.color.text_tertiary),
disabledActiveTickColor = MaterialTheme.colorScheme.secondary,
disabledInactiveTickColor = colorResource(id = R.color.text_tertiary)
)
)
AndroidView(
factory = { ctx ->
viewHolder = LinearLayout(ctx).apply {
layoutParams = LinearLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
id = R.id.map
}
map.init(
this@PlaceSettingsActivity,
this@PlaceSettingsActivity,
dark,
viewHolder
)
viewHolder
},
modifier = Modifier
.fillMaxWidth()
.requiredHeight(300.dp)
.padding(horizontal = 8.dp)
)
}
}
valueTo = MAX_RADIUS.toFloat()
valueFrom = MIN_RADIUS.toFloat()
stepSize = STEP.toFloat()
haloRadius = 0
value = (place.radius / STEP * STEP).roundToInt().toFloat()
}
slider.addOnChangeListener(this)
it.root
}
override fun hasChanges() = name.text.toString() != place.displayName
override fun hasChanges() = textState.value != place.displayName
|| selectedColor != place.color
|| selectedIcon.value != place.icon
override suspend fun save() {
val newName: String = name.text.toString()
val newName: String = textState.value
if (isNullOrEmpty(newName)) {
nameLayout.error = getString(R.string.name_cannot_be_empty)
errorState.value = getString(R.string.name_cannot_be_empty)
return
}
@ -121,7 +172,7 @@ class PlaceSettingsActivity : BaseListSettingsActivity(), MapFragment.MapFragmen
name = newName,
color = selectedColor,
icon = selectedIcon.value,
radius = slider.value.toInt(),
radius = sliderPos.floatValue.roundToInt(),
)
locationDao.update(place)
localBroadcastManager.broadcastRefresh()
@ -151,16 +202,13 @@ class PlaceSettingsActivity : BaseListSettingsActivity(), MapFragment.MapFragmen
map.setMarkers(listOf(place))
map.disableGestures()
map.movePosition(place.mapPosition, false)
updateGeofenceCircle()
updateGeofenceCircle(sliderPos.floatValue)
}
override fun onPlaceSelected(place: Place) {}
override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) {
updateGeofenceCircle()
}
private fun updateGeofenceCircle() {
val radius = slider.value.toDouble()
private fun updateGeofenceCircle(radius: Float) {
val radius = radius.toDouble()
val zoom = when (radius) {
in 0f..300f -> 15f
in 300f..500f -> 14.5f

@ -6,27 +6,21 @@
package org.tasks.activities
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.inputmethod.InputMethodManager
import androidx.core.widget.addTextChangedListener
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import androidx.activity.compose.setContent
import com.todoroo.astrid.activity.MainActivity
import com.todoroo.astrid.activity.TaskListFragment
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.update
import org.tasks.LocalBroadcastManager
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.data.dao.TagDao
import org.tasks.data.dao.TagDataDao
import org.tasks.data.entity.TagData
import org.tasks.databinding.ActivityTagSettingsBinding
import org.tasks.extensions.Context.hideKeyboard
import org.tasks.filters.TagFilter
import org.tasks.themes.TasksIcons
import org.tasks.themes.TasksTheme
import javax.inject.Inject
@AndroidEntryPoint
@ -35,9 +29,6 @@ class TagSettingsActivity : BaseListSettingsActivity() {
@Inject lateinit var tagDao: TagDao
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
private lateinit var name: TextInputEditText
private lateinit var nameLayout: TextInputLayout
private lateinit var tagData: TagData
private val isNewTag: Boolean
get() = tagData.id == null
@ -46,16 +37,20 @@ class TagSettingsActivity : BaseListSettingsActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
tagData = intent.getParcelableExtra(EXTRA_TAG_DATA) ?: TagData()
if (!isNewTag) textState.value = tagData.name!!
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
selectedColor = tagData.color ?: 0
selectedIcon.update { tagData.icon }
selectedIcon.value = tagData.icon ?: defaultIcon
}
setContent {
TasksTheme {
baseSettingsContent()
}
name.setText(tagData.name)
if (isNewTag) {
name.requestFocus()
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(name, InputMethodManager.SHOW_IMPLICIT)
}
updateTheme()
}
@ -67,7 +62,7 @@ class TagSettingsActivity : BaseListSettingsActivity() {
get() = if (isNew) getString(R.string.new_tag) else tagData.name!!
private val newName: String
get() = name.text.toString().trim { it <= ' ' }
get() = textState.value.trim { it <= ' ' }
private suspend fun clashes(newName: String): Boolean {
return ((isNewTag || !newName.equals(tagData.name, ignoreCase = true))
@ -77,11 +72,11 @@ class TagSettingsActivity : BaseListSettingsActivity() {
override suspend fun save() {
val newName = newName
if (isNullOrEmpty(newName)) {
nameLayout.error = getString(R.string.name_cannot_be_empty)
errorState.value = getString(R.string.name_cannot_be_empty)
return
}
if (clashes(newName)) {
nameLayout.error = getString(R.string.tag_already_exists)
errorState.value = getString(R.string.tag_already_exists)
return
}
if (isNewTag) {
@ -131,20 +126,10 @@ class TagSettingsActivity : BaseListSettingsActivity() {
}
override fun finish() {
hideKeyboard(name)
//hideKeyboard(name)
super.finish()
}
override fun bind() = ActivityTagSettingsBinding.inflate(layoutInflater).let {
name = it.name.apply {
addTextChangedListener(
onTextChanged = { _, _, _, _ -> nameLayout.error = null }
)
}
nameLayout = it.nameLayout
it.root
}
override suspend fun delete() {
val uuid = tagData.remoteId
tagDataDao.delete(tagData)
@ -159,3 +144,4 @@ class TagSettingsActivity : BaseListSettingsActivity() {
private const val EXTRA_TAG_UUID = "uuid" // $NON-NLS-1$
}
}

@ -1,32 +1,27 @@
package org.tasks.caldav
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.LinearLayout
import android.widget.ProgressBar
import androidx.core.widget.addTextChangedListener
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Composable
import androidx.lifecycle.lifecycleScope
import at.bitfire.dav4jvm.exception.HttpException
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import com.todoroo.astrid.activity.MainActivity
import com.todoroo.astrid.activity.TaskListFragment
import com.todoroo.astrid.service.TaskDeleter
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import org.tasks.R
import org.tasks.Strings.isNullOrEmpty
import org.tasks.activities.BaseListSettingsActivity
import org.tasks.compose.DeleteButton
import org.tasks.compose.ListSettings.Toaster
import org.tasks.data.UUIDHelper
import org.tasks.data.dao.CaldavDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar
import org.tasks.databinding.ActivityCaldavCalendarSettingsBinding
import org.tasks.extensions.Context.hideKeyboard
import org.tasks.filters.CaldavFilter
import org.tasks.themes.TasksIcons
import org.tasks.ui.DisplayableException
@ -37,27 +32,12 @@ abstract class BaseCaldavCalendarSettingsActivity : BaseListSettingsActivity() {
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var taskDeleter: TaskDeleter
private lateinit var root: LinearLayout
private lateinit var name: TextInputEditText
protected lateinit var nameLayout: TextInputLayout
protected lateinit var progressView: ProgressBar
protected var caldavCalendar: CaldavCalendar? = null
protected lateinit var caldavAccount: CaldavAccount
override val defaultIcon = TasksIcons.LIST
override fun bind() = ActivityCaldavCalendarSettingsBinding.inflate(layoutInflater).let {
root = it.rootLayout
name = it.name.apply {
addTextChangedListener(
onTextChanged = { _, _, _, _ -> nameLayout.error = null }
)
}
nameLayout = it.nameLayout
progressView = it.progressBar.progressBar
it.root
}
protected val snackbar = SnackbarHostState() // to be used by descendants
override fun onCreate(savedInstanceState: Bundle?) {
val intent = intent
@ -70,15 +50,10 @@ abstract class BaseCaldavCalendarSettingsActivity : BaseListSettingsActivity() {
}
if (savedInstanceState == null) {
if (caldavCalendar != null) {
name.setText(caldavCalendar!!.name)
textState.value = caldavCalendar!!.name ?: ""
selectedColor = caldavCalendar!!.color
selectedIcon.update { caldavCalendar?.icon }
}
selectedIcon.value = caldavCalendar?.icon ?: defaultIcon
}
if (caldavCalendar == null) {
name.requestFocus()
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(name, InputMethodManager.SHOW_IMPLICIT)
}
updateTheme()
}
@ -95,7 +70,7 @@ abstract class BaseCaldavCalendarSettingsActivity : BaseListSettingsActivity() {
}
val name = newName
if (isNullOrEmpty(name)) {
nameLayout.error = getString(R.string.name_cannot_be_empty)
errorState.value = getString(R.string.name_cannot_be_empty)
return
}
when {
@ -121,17 +96,11 @@ abstract class BaseCaldavCalendarSettingsActivity : BaseListSettingsActivity() {
caldavAccount: CaldavAccount, caldavCalendar: CaldavCalendar
)
private fun showProgressIndicator() {
progressView.visibility = View.VISIBLE
}
private fun showProgressIndicator() { showProgress.value = true }
private fun hideProgressIndicator() {
progressView.visibility = View.GONE
}
private fun hideProgressIndicator() { showProgress.value = false }
protected fun requestInProgress(): Boolean {
return progressView.visibility == View.VISIBLE
}
protected fun requestInProgress(): Boolean = showProgress.value
protected fun requestFailed(t: Throwable) {
hideProgressIndicator()
@ -146,17 +115,11 @@ abstract class BaseCaldavCalendarSettingsActivity : BaseListSettingsActivity() {
}
private fun showSnackbar(resId: Int, vararg formatArgs: Any) {
showSnackbar(getString(resId, *formatArgs))
lifecycleScope.launch { snackbar.showSnackbar( getString(resId, *formatArgs) ) }
}
private fun showSnackbar(message: String?) {
val snackbar = Snackbar.make(root, message!!, 8000)
.setTextColor(getColor(R.color.snackbar_text_color))
.setActionTextColor(getColor(R.color.snackbar_action_color))
snackbar
.view
.setBackgroundColor(getColor(R.color.snackbar_background))
snackbar.show()
lifecycleScope.launch { snackbar.showSnackbar( message!! ) }
}
protected suspend fun createSuccessful(url: String?) {
@ -202,10 +165,10 @@ abstract class BaseCaldavCalendarSettingsActivity : BaseListSettingsActivity() {
private fun iconChanged(): Boolean = selectedIcon.value != caldavCalendar!!.icon
private val newName: String
get() = name.text.toString().trim { it <= ' ' }
get() = textState.value.trim { it <= ' '}
override fun finish() {
hideKeyboard(name)
// hideKeyboard(name)
super.finish()
}
@ -234,6 +197,18 @@ abstract class BaseCaldavCalendarSettingsActivity : BaseListSettingsActivity() {
}
}
@Composable
fun baseCaldavSettingsContent (
optionButton: @Composable () -> Unit = { if (!isNew) DeleteButton { promptDelete() } },
extensionContent: @Composable ColumnScope.() -> Unit = {}
) {
baseSettingsContent (
optionButton = optionButton,
extensionContent = extensionContent
)
Toaster(state = snackbar)
}
companion object {
const val EXTRA_CALDAV_CALENDAR = "extra_caldav_calendar"
const val EXTRA_CALDAV_ACCOUNT = "extra_caldav_account"

@ -1,20 +1,29 @@
package org.tasks.caldav
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.core.view.isVisible
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
import org.tasks.R
import org.tasks.compose.Constants
import org.tasks.compose.ListSettingsComposables.PrincipalList
import org.tasks.compose.ShareInvite.ShareInviteDialog
import org.tasks.data.PrincipalWithAccess
@ -36,10 +45,14 @@ class CaldavCalendarSettingsActivity : BaseCaldavCalendarSettingsActivity() {
private val viewModel: CaldavCalendarViewModel by viewModels()
private var principalsList: MutableState<List<PrincipalWithAccess>> = mutableStateOf( emptyList<PrincipalWithAccess>().toMutableList())
private val removeDialog = mutableStateOf<PrincipalWithAccess?>(null)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.inFlight.observe(this) { progressView.isVisible = it }
viewModel.inFlight.observe(this) { showProgress.value = it }
viewModel.error.observe(this) { throwable ->
throwable?.let {
requestFailed(it)
@ -51,22 +64,63 @@ class CaldavCalendarSettingsActivity : BaseCaldavCalendarSettingsActivity() {
finish()
}
caldavCalendar?.takeIf { it.id > 0 }?.let {
findViewById<ComposeView>(R.id.people).setContent {
TasksTheme(theme = tasksTheme.themeBase.index) {
val principals = principalDao.getPrincipals(it.id).collectAsStateWithLifecycle(initialValue = emptyList()).value
setContent {
TasksTheme {
Box(contentAlignment = Alignment.TopStart) {// Box to layout FAB over main content
baseCaldavSettingsContent {
caldavCalendar?.takeIf { it.id > 0 }?.let { calendar->
val principals = principalDao.getPrincipals(calendar.id).collectAsStateWithLifecycle(initialValue = emptyList()).value
PrincipalList(
principals = principals,
onRemove = if (canRemovePrincipals) { { onRemove(it) } } else null,
)
}
if (principalsList.value.isNotEmpty())
PrincipalList(
principalsList.value,
onRemove = if (canRemovePrincipals) ::onRemove else null
)
}
removeDialog.value?.let { principal ->
AlertDialog(
onDismissRequest = { removeDialog.value = null },
confirmButton = {
Constants.TextButton(text = R.string.ok) {
removePrincipal(principal)
removeDialog.value = null
}
},
dismissButton = {
Constants.TextButton(text = R.string.cancel) {
removeDialog.value = null
}
},
title = {
Text(
stringResource(id = R.string.remove_user),
style = MaterialTheme.typography.headlineSmall
)
},
text = {
Text(
text = stringResource(
R.string.remove_user_confirmation,
principal.name,
caldavCalendar?.name ?: ""
),
style = MaterialTheme.typography.bodyMedium
)
}
)
}
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.BottomEnd
) {
if (caldavAccount.canShare && (isNew || caldavCalendar?.access == ACCESS_OWNER)) {
findViewById<ComposeView>(R.id.fab)
.apply { isVisible = true }
.setContent {
TasksTheme(theme = tasksTheme.themeBase.index) {
val openDialog = rememberSaveable { mutableStateOf(false) }
ShareInviteDialog(
openDialog,
@ -79,32 +133,27 @@ class CaldavCalendarSettingsActivity : BaseCaldavCalendarSettingsActivity() {
}
FloatingActionButton(
onClick = { openDialog.value = true },
containerColor = MaterialTheme.colorScheme.primary
modifier = Modifier.padding(Constants.KEYLINE_FIRST),
containerColor = MaterialTheme.colorScheme.secondary
) {
Icon(
painter = painterResource(R.drawable.ic_outline_person_add_24),
contentDescription = null,
tint = MaterialTheme.colorScheme.onPrimary,
tint = MaterialTheme.colorScheme.onSecondary,
)
}
}
}
}
}
}
private val canRemovePrincipals: Boolean
get() = caldavCalendar?.access == ACCESS_OWNER && caldavAccount.canRemovePrincipal
private fun onRemove(principal: PrincipalWithAccess) {
if (requestInProgress()) {
return
}
dialogBuilder
.newDialog(R.string.remove_user)
.setMessage(R.string.remove_user_confirmation, principal.name, caldavCalendar?.name)
.setNegativeButton(R.string.cancel, null)
.setPositiveButton(R.string.ok) { _, _ -> removePrincipal(principal) }
.show()
if (requestInProgress()) return
removeDialog.value = principal
}
private fun removePrincipal(principal: PrincipalWithAccess) = lifecycleScope.launch {

@ -1,21 +1,32 @@
package org.tasks.caldav
import android.os.Bundle
import androidx.activity.compose.setContent
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.runBlocking
import org.tasks.R
import org.tasks.compose.DeleteButton
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.dao.CaldavDao
import org.tasks.themes.TasksTheme
@AndroidEntryPoint
class LocalListSettingsActivity : BaseCaldavCalendarSettingsActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
toolbar.menu.findItem(R.id.delete)?.isVisible =
runBlocking { caldavDao.getCalendarsByAccount(CaldavDao.LOCAL).size > 1 }
val canDelete = runBlocking { caldavDao.getCalendarsByAccount(CaldavDao.LOCAL).size > 1 }
setContent {
TasksTheme {
baseCaldavSettingsContent (
optionButton = { if (!isNew && canDelete) DeleteButton { promptDelete() } }
)
}
}
}
override suspend fun createCalendar(caldavAccount: CaldavAccount, name: String, color: Int) =

@ -0,0 +1,199 @@
package org.tasks.compose
import androidx.compose.animation.core.FastOutLinearInEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListItemInfo
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.zIndex
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlin.math.max
import kotlin.math.min
/**
* Drag - drop to reorder elements of LazyColumn
*
* Implementation is based on:
* https://github.com/realityexpander/DragDropColumnCompose
*
* Scheme of use:
* 1. Hoist state of the LazyColumn (create your own and set it as LazyColumn parameter)
* 2. Create and remember DragDropState object by call to "rememberDragDropState"
* 3. Use Modifier.doDrag in the LazyColumn
* 4. enclose LazyList items into DraggableItem
* **/
class DragDropState internal constructor(
val state: LazyListState,
private val scope: CoroutineScope,
private val confirmDrag: (Int) -> Boolean,
private val complete: () -> Unit,
private val swap: (Int, Int) -> Unit,
) {
/* primary ID of the item being dragged */
var draggedItemIndex by mutableStateOf<Int?>(null)
private var draggedDistance by mutableFloatStateOf(0f)
private var draggingElementOffset: Int = 0 // cached drugged element offset and size
private var draggingElementSize: Int = -1 // size must not be negative when dragging is in progress
private var overscrollJob by mutableStateOf<Job?>( null )
/* sibling of draggingElementOffset, not cached, for use in animation */
internal val draggingItemOffset: Float
get() = state.layoutInfo.visibleItemsInfo
.firstOrNull { it.index == draggedItemIndex }
?.let { item ->
draggingElementOffset + draggedDistance - item.offset
} ?: 0f
fun itemAtOffset(offsetY: Float): LazyListItemInfo? =
state.layoutInfo.visibleItemsInfo.firstOrNull {
item -> offsetY.toInt() in item.offset..(item.offset + item.size)
}
fun startDragging(item: LazyListItemInfo?) {
//Log.d("HADY", "start dragging ${item}")
if (item != null && confirmDrag(item.index)) {
draggedItemIndex = item.index
draggingElementOffset = item.offset
draggingElementSize = item.size
assert(item.size >= 0) { "Invalid size of element ${item.size}" }
}
}
fun stopDragging() {
draggedDistance = 0f
draggedItemIndex = null
draggingElementOffset = 0
draggingElementSize = -1
overscrollJob?.cancel()
complete()
}
fun onDrag(offset: Offset) {
draggedDistance += offset.y
if (draggedItemIndex != null) {
assert(draggingElementSize >= 0) { "FATAL: Invalid dragging element" }
val startOffset = draggingElementOffset + draggedDistance
val endOffset = startOffset + draggingElementSize
val draggedIndex = draggedItemIndex
val dragged = draggedIndex?.let { index ->
state.layoutInfo.visibleItemsInfo.getOrNull(
index - state.layoutInfo.visibleItemsInfo.first().index
)
}
if (dragged != null) {
val up = (startOffset - dragged.offset) > 0
val hovered =
if (up) itemAtOffset(startOffset + 0.1f * draggingElementSize)
else itemAtOffset(endOffset - 0.1f * draggingElementSize)
if (hovered != null) {
scope.launch { swap(draggedIndex, hovered.index) }
draggedItemIndex = hovered.index
}
if (overscrollJob?.isActive != true) {
val overscroll = when {
draggedDistance > 0 -> max(endOffset - state.layoutInfo.viewportEndOffset+50f, 0f)
draggedDistance < 0 -> min(startOffset - state.layoutInfo.viewportStartOffset-50f, 0f)
else -> 0f
}
if (overscroll != 0f) {
overscrollJob = scope.launch {
state.animateScrollBy(
overscroll * 1.3f, tween(easing = FastOutLinearInEasing)
)
}
}
}
}
}
} /* end onDrag */
} /* end DragDropState */
@Composable
fun rememberDragDropState(
lazyListState: LazyListState,
confirmDrag: (Int) -> Boolean = { true },
completeDragDrop: () -> Unit,
doSwap: (Int, Int) -> Unit
): DragDropState {
val scope = rememberCoroutineScope()
val state = remember(lazyListState) {
DragDropState(
state = lazyListState,
swap = doSwap,
complete = completeDragDrop,
scope = scope,
confirmDrag = confirmDrag
)
}
return state
}
fun Modifier.doDrag(dragDropState: DragDropState): Modifier =
this.pointerInput(dragDropState) {
detectDragGesturesAfterLongPress(
onDragStart = { offset ->
dragDropState.startDragging(dragDropState.itemAtOffset(offset.y))
},
onDrag = { change, offset ->
change.consume()
dragDropState.onDrag(offset)
},
onDragEnd = { dragDropState.stopDragging() },
onDragCancel = { dragDropState.stopDragging() }
)
}
@ExperimentalFoundationApi
@Composable
fun LazyItemScope.DraggableItem(
dragDropState: DragDropState,
index: Int,
modifier: Modifier = Modifier,
content: @Composable (isDragging: Boolean) -> Unit
) {
val current: Float by animateFloatAsState(
targetValue = dragDropState.draggingItemOffset * 0.67f
)
val dragging = index == dragDropState.draggedItemIndex
val draggingModifier = if (dragging) {
Modifier
.zIndex(1f)
.graphicsLayer { translationY = current }
} else {
Modifier.animateItemPlacement(
tween(easing = FastOutLinearInEasing)
)
}
Box(modifier = modifier.then(draggingModifier)) {
content(dragging)
}
}

@ -0,0 +1,496 @@
package org.tasks.compose
/**
* Composables for FilterSettingActivity
**/
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Abc
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.FloatingActionButton
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableIntState
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.core.os.ConfigurationCompat
import com.todoroo.astrid.core.CriterionInstance
import org.tasks.R
import org.tasks.compose.ListSettings.SettingRow
import org.tasks.compose.SwipeOut.SwipeOut
import org.tasks.extensions.formatNumber
import org.tasks.themes.TasksTheme
import java.util.Locale
@Composable
@Preview (showBackground = true)
private fun CriterionTypeSelectPreview () {
TasksTheme {
FilterCondition.SelectCriterionType(
title = "Select criterion type",
selected = 1,
types = listOf("AND", "OR", "NOT"),
onCancel = { /*TODO*/ }) {
}
}
}
@Composable
@Preview (showBackground = true)
private fun InputTextPreview () {
TasksTheme {
FilterCondition.InputTextOption(title = "Task name contains...", onCancel = { /*TODO*/ }
) {
}
}
}
@Composable
@Preview (showBackground = true)
private fun SwipeOutDecorationPreview () {
TasksTheme {
Box(modifier = Modifier
.height(56.dp)
.fillMaxWidth()) {
FilterCondition.SwipeOutDecoration()
}
}
}
@Composable
@Preview (showBackground = true)
private fun FabPreview () {
TasksTheme {
FilterCondition.NewCriterionFAB(
isExtended = remember { mutableStateOf(true) }
) {
}
}
}
object FilterCondition {
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun FilterCondition(
items: SnapshotStateList<CriterionInstance>,
onDelete: (Int) -> Unit,
doSwap: (Int, Int) -> Unit,
onComplete: () -> Unit,
onClick: (String) -> Unit
) {
val getIcon: (CriterionInstance) -> Int = { criterion ->
when (criterion.type) {
CriterionInstance.TYPE_ADD -> R.drawable.ic_call_split_24px
CriterionInstance.TYPE_SUBTRACT -> R.drawable.ic_outline_not_interested_24px
CriterionInstance.TYPE_INTERSECT -> R.drawable.ic_outline_add_24px
else -> {
0
} /* assert */
}
}
val listState = rememberLazyListState()
val dragDropState = rememberDragDropState(
lazyListState = listState,
confirmDrag = { index -> index != 0 },
completeDragDrop = onComplete,
) { fromIndex, toIndex ->
if (fromIndex != 0 && toIndex != 0) doSwap(fromIndex, toIndex)
}
Row {
Text(
text = stringResource(id = R.string.custom_filter_criteria),
color = MaterialTheme.colorScheme.secondary,
modifier = Modifier
.fillMaxWidth()
.padding(Constants.KEYLINE_FIRST)
)
}
Row {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.doDrag(dragDropState),
userScrollEnabled = true,
state = listState
) {
itemsIndexed(
items = items,
key = { _, item -> item.id + " " + item.type + " " + item.end}
) { index, criterion ->
if (index == 0) {
FilterConditionRow(criterion, false, getIcon, onClick)
} else {
DraggableItem(
dragDropState = dragDropState, index = index
) { dragging ->
SwipeOut(
decoration = { SwipeOutDecoration() },
onSwipe = { index -> onDelete(index) },
index = index
) {
FilterConditionRow(criterion, dragging, getIcon, onClick)
}
}
}
}
}
}
} /* FilterCondition */
@Composable
private fun FilterConditionRow(
criterion: CriterionInstance,
dragging: Boolean,
getIcon: (CriterionInstance) -> Int,
onClick: (String) -> Unit
) {
HorizontalDivider(
color = when (criterion.type) {
CriterionInstance.TYPE_ADD -> Color.Gray
else -> Color.Transparent
}
)
val modifier =
if (dragging) Modifier.background(Color.LightGray)
else Modifier
SettingRow(
modifier = modifier.clickable { onClick(criterion.id) },
left = {
Box(
modifier = Modifier.size(56.dp),
contentAlignment = Alignment.Center
)
{
if (criterion.type != CriterionInstance.TYPE_UNIVERSE) {
Icon(
modifier = Modifier.padding(Constants.KEYLINE_FIRST),
painter = painterResource(id = getIcon(criterion)),
contentDescription = null
)
}
}
},
center = {
Text(
text = criterion.titleFromCriterion,
fontSize = 18.sp,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(start = 16.dp, top = 16.dp, bottom = 16.dp)
)
},
right = {
val context = LocalContext.current
val locale = remember {
ConfigurationCompat
.getLocales(context.resources.configuration)
.get(0)
?: Locale.getDefault()
}
Text(
text = locale.formatNumber(criterion.end),
modifier = Modifier.padding(end = Constants.KEYLINE_FIRST),
color = Color.Gray,
fontSize = 14.sp,
textAlign = TextAlign.End
)
}
)
}
@Composable
fun SwipeOutDecoration() {
Box(
modifier = Modifier
.fillMaxSize()
.background(colorResource(id = org.tasks.kmp.R.color.red_a400))
//.background(MaterialTheme.colorScheme.secondary)
) {
@Composable
fun deleteIcon() {
Icon(
modifier = Modifier.padding(horizontal = Constants.KEYLINE_FIRST),
imageVector = Icons.Outlined.Delete,
contentDescription = "Delete",
tint = Color.White.copy(alpha = 0.6f)
)
}
Row(
modifier = Modifier
.fillMaxSize()
.height(56.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
deleteIcon()
deleteIcon()
}
}
} /* end SwipeOutDecoration */
@Composable
fun NewCriterionFAB(
isExtended: MutableState<Boolean>,
onClick: () -> Unit
) {
Box( // lays out over main content as a space to layout FAB
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.BottomEnd
) {
FloatingActionButton(
onClick = onClick,
modifier = Modifier.padding(16.dp),
shape = RoundedCornerShape(50),
containerColor = MaterialTheme.colorScheme.secondary,
contentColor = Color.White,
) {
val extended = isExtended.value
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = Icons.Outlined.Add,
contentDescription = "New Criteria",
modifier = Modifier.padding(
start = if (extended) 16.dp else 0.dp
)
)
if (extended)
Text(
text = LocalContext.current.getString(R.string.CFA_button_add),
modifier = Modifier.padding(end = 16.dp)
)
}
} /* end FloatingActionButton */
}
} /* end NewCriterionFAB */
@Composable
fun SelectCriterionType(
title: String,
selected: Int,
types: List<String>,
onCancel: () -> Unit,
help: () -> Unit = {},
onSelected: (Int) -> Unit
) {
val selected = remember { mutableIntStateOf(selected) }
Dialog(onDismissRequest = onCancel)
{
Card(
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.background)
) {
Column(modifier = Modifier
.padding(horizontal = Constants.KEYLINE_FIRST)
.padding(top = Constants.HALF_KEYLINE)
) {
Text(
text = title,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
.padding(top = 16.dp)
)
Spacer(modifier = Modifier.height(Constants.HALF_KEYLINE))
ToggleGroup(items = types, selected = selected)
Row(
modifier = Modifier.height(48.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(contentAlignment = Alignment.CenterStart) {
Constants.TextButton(text = R.string.help, onClick = help)
}
Box(
contentAlignment = Alignment.CenterEnd,
modifier = Modifier.fillMaxWidth()
) {
Row {
Constants.TextButton(text = R.string.cancel, onClick = onCancel)
Constants.TextButton(text = R.string.ok) { onSelected(selected.intValue) }
}
}
}
}
}
}
} /* end SelectCriterionType */
@Composable
fun ToggleGroup(
items: List<String>,
selected: MutableIntState = remember { mutableIntStateOf(0) }
) {
assert(selected.intValue in items.indices)
Box(
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
contentAlignment = Alignment.Center
) {
Row {
for (index in items.indices) {
val highlight = (index == selected.intValue)
val color =
if (highlight) MaterialTheme.colorScheme.secondary.copy(alpha = 0.5f)
else MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f)
OutlinedButton(
onClick = { selected.intValue = index },
border = BorderStroke(1.dp, SolidColor(color.copy(alpha = 0.5f))),
colors = ButtonDefaults.outlinedButtonColors(
containerColor = color.copy(alpha = 0.2f),
contentColor = MaterialTheme.colorScheme.onBackground),
shape = RoundedCornerShape(Constants.HALF_KEYLINE)
) {
Text(items[index])
}
if (index<items.size-1) Spacer(modifier = Modifier.size(2.dp))
}
}
}
} /* end ToggleGroup */
@Composable
fun SelectFromList(
names: List<String>,
title: String? = null,
onCancel: () -> Unit,
onSelected: (Int) -> Unit
) {
Dialog(onDismissRequest = onCancel) {
Card(
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.background)
) {
Column(
modifier = Modifier
.padding(horizontal = Constants.KEYLINE_FIRST)
.padding(bottom = Constants.KEYLINE_FIRST)
) {
title?.let { title ->
Text(
text = title,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Medium,
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
.padding(top = Constants.KEYLINE_FIRST)
)
}
names.forEachIndexed { index, name ->
Text(
text = name,
color = MaterialTheme.colorScheme.onSurface,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
.padding(top = Constants.KEYLINE_FIRST)
.clickable { onSelected(index) }
)
}
}
}
}
} /* end SelectFromList */
@Composable
fun InputTextOption(
title: String,
onCancel: () -> Unit,
onDone: (String) -> Unit
) {
val text = remember { mutableStateOf("") }
AlertDialog(
onDismissRequest = onCancel,
confirmButton = {
Constants.TextButton(
text = R.string.ok,
onClick = { onDone(text.value) })
},
dismissButton = { Constants.TextButton(text = R.string.cancel, onClick = onCancel) },
text = {
Column(modifier = Modifier.fillMaxWidth()) {
Text(
title,
style = MaterialTheme.typography.headlineSmall,
)
Spacer(Modifier.height(Constants.KEYLINE_FIRST))
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = text.value,
label = { Text(title) },
onValueChange = { text.value = it },
leadingIcon = {
Icon(
imageVector = Icons.Outlined.Abc,
contentDescription = null
)
},
textStyle = MaterialTheme.typography.bodyMedium,
colors = Constants.textFieldColors(),
)
}
}
)
} /* end InputTextOption */
}

@ -0,0 +1,374 @@
package org.tasks.compose
/**
* Composables for BaseListSettingActivity
*/
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.Snackbar
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
import org.tasks.R
import org.tasks.compose.components.TasksIcon
import org.tasks.themes.TasksIcons
import org.tasks.themes.TasksTheme
@Composable
@Preview (showBackground = true)
private fun TitleBarPreview() {
TasksTheme {
ListSettings.Toolbar(
title = "Tollbar title",
save = { /*TODO*/ }, optionButton = { DeleteButton {} }
)
}
}
@Composable
@Preview(showBackground = true)
private fun PromptActionPreview() {
TasksTheme {
ListSettings.PromptAction(
showDialog = remember { mutableStateOf(true) },
title = "Delete list?",
onAction = { /*TODO*/ })
}
}
@Composable
@Preview (showBackground = true)
private fun IconSelectPreview () {
TasksTheme {
ListSettings.SelectIconRow(
icon = TasksIcons.FILTER_LIST,
selectIcon = {}
)
}
}
@Composable
@Preview (showBackground = true)
private fun ColorSelectPreview () {
TasksTheme {
ListSettings.SelectColorRow(
color = remember { mutableStateOf(Color.Red) },
selectColor = {},
clearColor = {}
)
}
}
object ListSettings {
@Composable
fun Toolbar(
title: String,
save: () -> Unit,
optionButton: @Composable () -> Unit,
) {
/* Hady: reminder for the future
val activity = LocalView.current.context as Activity
activity.window.statusBarColor = colorResource(id = R.color.drawer_color_selected).toArgb()
*/
Surface(
shadowElevation = 4.dp,
color = colorResource(id = R.color.content_background),
contentColor = colorResource(id = R.color.text_primary),
modifier = Modifier.requiredHeight(56.dp)
)
{
Row(verticalAlignment = Alignment.CenterVertically) {
IconButton(onClick = save, modifier = Modifier.size(56.dp)) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_outline_save_24px),
contentDescription = stringResource(id = R.string.save),
)
}
Text(
text = title,
fontWeight = FontWeight.Medium,
fontSize = 20.sp,
modifier = Modifier
.weight(0.9f)
.padding(start = Constants.KEYLINE_FIRST)
)
optionButton()
}
}
} /* ToolBar */
@Composable
fun ProgressBar(showProgress: State<Boolean>) {
Box(modifier = Modifier
.fillMaxWidth()
.requiredHeight(3.dp))
{
if (showProgress.value) {
LinearProgressIndicator(
modifier = Modifier.fillMaxSize(),
trackColor = LocalContentColor.current.copy(alpha = 0.3f), //Color.LightGray,
color = colorResource(org.tasks.kmp.R.color.red_a400)
)
}
}
}
@Composable
fun TitleInput(
text: MutableState<String>,
error: MutableState<String>,
requestKeyboard: Boolean,
modifier: Modifier = Modifier,
label: String = stringResource(R.string.display_name),
errorState: Color = MaterialTheme.colorScheme.secondary,
activeState: Color = LocalContentColor.current.copy(alpha = 0.75f),
inactiveState: Color = LocalContentColor.current.copy(alpha = 0.3f),
) {
val keyboardController = LocalSoftwareKeyboardController.current
val requester = remember { FocusRequester() }
val focused = remember { mutableStateOf(false) }
val labelColor = when {
(error.value != "") -> errorState
(focused.value) -> activeState
else -> inactiveState
}
val dividerColor = if (focused.value) errorState else labelColor
val labelText = if (error.value != "") error.value else label
Row (modifier = modifier)
{
Column {
Text(
modifier = Modifier.padding(top = 18.dp, bottom = 4.dp),
text = labelText,
fontSize = 12.sp,
letterSpacing = 0.sp,
fontWeight = FontWeight.Medium,
color = labelColor
)
BasicTextField(
value = text.value,
textStyle = TextStyle(
fontSize = LocalTextStyle.current.fontSize,
color = LocalContentColor.current
),
onValueChange = {
text.value = it
if (error.value != "") error.value = ""
},
cursorBrush = SolidColor(errorState), // SolidColor(LocalContentColor.current),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 3.dp)
.focusRequester(requester)
.onFocusChanged { focused.value = (it.isFocused) }
)
HorizontalDivider(
color = dividerColor,
modifier = Modifier.padding(bottom = 8.dp)
)
}
}
if (requestKeyboard) {
LaunchedEffect(null) {
requester.requestFocus()
delay(30) // Workaround. Otherwise keyboard don't show in 4/5 tries
keyboardController?.show()
}
}
} /* TextInput */
@Composable
fun SelectColorRow(color: State<Color>, selectColor: () -> Unit, clearColor: () -> Unit) =
SettingRow(
modifier = Modifier.clickable(onClick = selectColor),
left = {
IconButton(onClick = { selectColor() }) {
if (color.value == Color.Unspecified) {
Icon(
imageVector = ImageVector.vectorResource(R.drawable.ic_outline_not_interested_24px),
tint = colorResource(R.color.icon_tint_with_alpha),
contentDescription = null
)
} else {
val borderColor = colorResource(R.color.icon_tint_with_alpha) // colorResource(R.color.text_tertiary)
Box(
modifier = Modifier.size(56.dp),
contentAlignment = Alignment.Center
) {
Canvas(modifier = Modifier.size(24.dp)) {
drawCircle(color = color.value)
drawCircle(color = borderColor, style = Stroke(width = 4.0f)
)
}
}
}
}
},
center = {
Text(
text = LocalContext.current.getString(R.string.color),
modifier = Modifier.padding(start = Constants.KEYLINE_FIRST)
)
},
right = {
if (color.value != Color.Unspecified) {
IconButton(onClick = clearColor) {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_outline_clear_24px),
contentDescription = null
)
}
}
}
)
@Composable
fun SelectIconRow(icon: String, selectIcon: () -> Unit) =
SettingRow(
modifier = Modifier.clickable(onClick = selectIcon),
left = {
IconButton(onClick = selectIcon) {
TasksIcon(
label = icon,
tint = colorResource(R.color.icon_tint_with_alpha)
)
}
},
center = {
Text(
text = LocalContext.current.getString(R.string.icon),
modifier = Modifier.padding(start = Constants.KEYLINE_FIRST)
)
}
)
@Composable
fun SettingRow(
left: @Composable () -> Unit,
center: @Composable () -> Unit,
right: @Composable (() -> Unit)? = null,
modifier: Modifier = Modifier
) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) {
Box (modifier = Modifier.size(56.dp), contentAlignment = Alignment.Center) {
left()
}
Box (
modifier = Modifier
.height(56.dp)
.weight(1f), contentAlignment = Alignment.CenterStart
) {
center()
}
right?.let {
Box (modifier = Modifier.size(56.dp), contentAlignment = Alignment.Center) {
it.invoke()
}
}
}
}
@Composable
fun SettingsSurface(content: @Composable ColumnScope.() -> Unit) {
ProvideTextStyle(LocalTextStyle.current.copy(fontSize = 18.sp)) {
Surface(
color = colorResource(id = R.color.window_background),
contentColor = colorResource(id = R.color.text_primary)
) {
Column(modifier = Modifier.fillMaxSize()) { content() }
}
}
}
@Composable
fun Toaster(state: SnackbarHostState) {
SnackbarHost(state) { data ->
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Snackbar(
modifier = Modifier.padding(horizontal = 24.dp),
shape = RoundedCornerShape(10.dp),
containerColor = colorResource(id = R.color.snackbar_background),
contentColor = colorResource(id = R.color.snackbar_text_color),
) {
Text(text = data.visuals.message, fontSize = 18.sp)
}
}
}
}
@Composable
fun PromptAction(
showDialog: MutableState<Boolean>,
title: String,
onAction: () -> Unit,
onCancel: () -> Unit = { showDialog.value = false }
) {
if (showDialog.value) {
AlertDialog(
onDismissRequest = onCancel,
title = { Text(title, style = MaterialTheme.typography.headlineSmall) },
confirmButton = { Constants.TextButton(text = R.string.ok, onClick = onAction) },
dismissButton = { Constants.TextButton(text = R.string.cancel, onClick = onCancel) }
)
}
}
}

@ -0,0 +1,94 @@
package org.tasks.compose
/**
* Simple Swipe-to-delete implementation
*/
import androidx.compose.animation.core.exponentialDecay
import androidx.compose.animation.core.tween
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.AnchoredDraggableState
import androidx.compose.foundation.gestures.DraggableAnchors
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.anchoredDraggable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import org.tasks.R
import kotlin.math.roundToInt
object SwipeOut {
private enum class Anchors { Left, Center, Right }
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun SwipeOut(
modifier: Modifier = Modifier,
index: Int,
onSwipe: (Int) -> Unit,
decoration: @Composable BoxScope.() -> Unit = {},
content: @Composable BoxScope.() -> Unit
) {
val screenWidthPx =
with(LocalDensity.current) {
LocalConfiguration.current.screenWidthDp.dp.roundToPx().toFloat()
}
val dragState: AnchoredDraggableState<Anchors> = remember {
AnchoredDraggableState(
initialValue = Anchors.Center,
anchors = DraggableAnchors {
Anchors.Left at -screenWidthPx * 3/4
Anchors.Center at 0f
Anchors.Right at screenWidthPx * 3/4
},
positionalThreshold = { _ -> screenWidthPx/3 },
velocityThreshold = { 100f },
snapAnimationSpec = tween(),
decayAnimationSpec = exponentialDecay()
)
}
if (dragState.currentValue == Anchors.Left || dragState.currentValue == Anchors.Right) {
onSwipe(index)
}
Box( /* container for swipeable and it's background decoration */
modifier = modifier.fillMaxSize(),
contentAlignment = Alignment.CenterStart
) {
decoration()
Box(
modifier = modifier
.fillMaxWidth()
.offset {
IntOffset(
x = dragState
.requireOffset()
.roundToInt(),
y = 0
)
}
.background(colorResource(id = R.color.content_background)) // MUST BE AFTER .offset modifier (?!?!)
.anchoredDraggable(state = dragState, orientation = Orientation.Horizontal)
) {
content()
}
}
}
}

@ -1,11 +1,13 @@
package org.tasks.etebase
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import dagger.hilt.android.AndroidEntryPoint
import org.tasks.caldav.BaseCaldavCalendarSettingsActivity
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar
import org.tasks.themes.TasksTheme
@AndroidEntryPoint
class EtebaseCalendarSettingsActivity : BaseCaldavCalendarSettingsActivity() {
@ -19,6 +21,12 @@ class EtebaseCalendarSettingsActivity : BaseCaldavCalendarSettingsActivity() {
createCalendarViewModel.observe(this, this::createSuccessful, this::requestFailed)
deleteCalendarViewModel.observe(this, this::onDeleted, this::requestFailed)
updateCalendarViewModel.observe(this, { updateCalendar() }, this::requestFailed)
setContent {
TasksTheme {
baseCaldavSettingsContent()
}
}
}
override suspend fun createCalendar(caldavAccount: CaldavAccount, name: String, color: Int) =

@ -1,10 +1,11 @@
package org.tasks.location
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import org.tasks.data.entity.Place
interface MapFragment {
fun init(activity: AppCompatActivity, callback: MapFragmentCallback, dark: Boolean)
fun init(activity: AppCompatActivity, callback: MapFragmentCallback, dark: Boolean, parent: ViewGroup? = null)
val mapPosition: MapPosition?

@ -2,6 +2,7 @@ package org.tasks.location
import android.annotation.SuppressLint
import android.content.Context
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
@ -31,7 +32,7 @@ class OsmMapFragment @Inject constructor(
private var locationOverlay: MyLocationNewOverlay? = null
private var circle: Polygon? = null
override fun init(activity: AppCompatActivity, callback: MapFragmentCallback, dark: Boolean) {
override fun init(activity: AppCompatActivity, callback: MapFragmentCallback, dark: Boolean, parent: ViewGroup?) {
this.callback = callback
Configuration.getInstance()
.load(activity, PreferenceManager.getDefaultSharedPreferences(activity))
@ -46,7 +47,8 @@ class OsmMapFragment @Inject constructor(
val copyright = CopyrightOverlay(activity)
copyright.setTextColor(ContextCompat.getColor(activity, R.color.text_primary))
overlays.add(copyright)
activity.findViewById<ViewGroup>(R.id.map).addView(this)
if (parent != null) parent.addView(this)
else activity.findViewById<ViewGroup>(R.id.map).addView(this)
}
callback.onMapReady(this)
}

@ -1,12 +1,19 @@
package org.tasks.opentasks
import android.os.Bundle
import android.view.View
import androidx.activity.compose.setContent
import androidx.lifecycle.lifecycleScope
import dagger.hilt.android.AndroidEntryPoint
import org.tasks.R
import kotlinx.coroutines.launch
import org.tasks.caldav.BaseCaldavCalendarSettingsActivity
import org.tasks.compose.ListSettings.ProgressBar
import org.tasks.compose.ListSettings.Toaster
import org.tasks.compose.ListSettings.SettingsSurface
import org.tasks.compose.ListSettings.Toolbar
import org.tasks.compose.ListSettings.SelectIconRow
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar
import org.tasks.themes.TasksTheme
@AndroidEntryPoint
class OpenTasksListSettingsActivity : BaseCaldavCalendarSettingsActivity() {
@ -14,9 +21,20 @@ class OpenTasksListSettingsActivity : BaseCaldavCalendarSettingsActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
toolbar.menu.findItem(R.id.delete).isVisible = false
nameLayout.visibility = View.GONE
colorRow.visibility = View.GONE
setContent {
TasksTheme {
SettingsSurface {
Toolbar(
title = toolbarTitle,
save = { lifecycleScope.launch { save() } },
optionButton = { },
)
ProgressBar(showProgress)
SelectIconRow(icon = selectedIcon.value?: defaultIcon) { showIconPicker() }
}
Toaster(state = snackbar)
}
} /* setContent */
}
override suspend fun createCalendar(caldavAccount: CaldavAccount, name: String, color: Int) {}

@ -3,6 +3,7 @@ package org.tasks.sync.microsoft
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import com.todoroo.astrid.activity.MainActivity
@ -12,6 +13,7 @@ import dagger.hilt.android.AndroidEntryPoint
import org.tasks.caldav.BaseCaldavCalendarSettingsActivity
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar
import org.tasks.themes.TasksTheme
@AndroidEntryPoint
class MicrosoftListSettingsActivity : BaseCaldavCalendarSettingsActivity() {
@ -44,6 +46,12 @@ class MicrosoftListSettingsActivity : BaseCaldavCalendarSettingsActivity() {
}
}
}
setContent {
TasksTheme {
baseCaldavSettingsContent()
}
}
}
override suspend fun createCalendar(caldavAccount: CaldavAccount, name: String, color: Int) =

@ -57,6 +57,11 @@
android:clipToPadding="false"
android:nestedScrollingEnabled="false"/>
<androidx.compose.ui.platform.ComposeView
android:id="@+id/filter_criteria_list"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>

@ -60,6 +60,7 @@ fun TasksTheme(
colorScheme = colorScheme.copy(
primary = Color(primary),
onPrimary = colorOnPrimary,
secondary = Color.Red, // Hady: Sorry for this hack, I believe the regular solution is planned
),
) {
content()

Loading…
Cancel
Save