diff --git a/app/generic/release/output-metadata.json b/app/generic/release/output-metadata.json new file mode 100644 index 000000000..0c89ff6c0 --- /dev/null +++ b/app/generic/release/output-metadata.json @@ -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" +} \ No newline at end of file diff --git a/app/src/googleplay/java/org/tasks/location/GoogleMapFragment.kt b/app/src/googleplay/java/org/tasks/location/GoogleMapFragment.kt index 64812246f..540dc84e6 100644 --- a/app/src/googleplay/java/org/tasks/location/GoogleMapFragment.kt +++ b/app/src/googleplay/java/org/tasks/location/GoogleMapFragment.kt @@ -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() - fragmentManager.beginTransaction().replace(R.id.map, mapFragment).commit() + if (parent == null) { + fragmentManager.beginTransaction().replace(R.id.map, mapFragment).commit() + } else { + fragmentManager.beginTransaction().add(parent, mapFragment, null).commit() + } } mapFragment.getMapAsync(this) } diff --git a/app/src/main/java/org/tasks/activities/BaseListSettingsActivity.kt b/app/src/main/java/org/tasks/activities/BaseListSettingsActivity.kt index d39d9a378..f6ae16961 100644 --- a/app/src/main/java/org/tasks/activities/BaseListSettingsActivity.kt +++ b/app/src/main/java/org/tasks/activities/BaseListSettingsActivity.kt @@ -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(null) + protected var selectedIcon = mutableStateOf(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(R.id.clear).apply { - setOnClickListener { clearColor() } - } - color = findViewById(R.id.color) - colorRow = findViewById(R.id.color_row).apply { - setOnClickListener { showThemePicker() } - } - findViewById(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(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) + + colorState.value = + if (selectedColor == 0) Color.Unspecified + else Color((colorProvider.getThemeColor(selectedColor, true)).primaryColor) - 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() + //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 { diff --git a/app/src/main/java/org/tasks/activities/FilterSettingsActivity.kt b/app/src/main/java/org/tasks/activities/FilterSettingsActivity.kt index a0ab73034..ce9cb77f3 100644 --- a/app/src/main/java/org/tasks/activities/FilterSettingsActivity.kt +++ b/app/src/main/java/org/tasks/activities/FilterSettingsActivity.kt @@ -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 = ArrayList() override val defaultIcon = TasksIcons.FILTER_LIST + private var criteria: SnapshotStateList = emptyList().toMutableStateList() + private val fabExtended = mutableStateOf(false) + private val editCriterionType: MutableState = mutableStateOf(null) + private val newCriterionTypes: MutableState?> = mutableStateOf(null) + private val newCriterionOptions: MutableState = 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) { - this.criteria = criteria + private fun setCriteria(criteriaList: List) { + 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 - } - 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() - } + newCriterionTypes.value = filterCriteriaProvider.all() } - 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().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,13 +252,139 @@ 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" const val EXTRA_TITLE = "extra_title" diff --git a/app/src/main/java/org/tasks/activities/GoogleTaskListSettingsActivity.kt b/app/src/main/java/org/tasks/activities/GoogleTaskListSettingsActivity.kt index 795c3ba00..a09632a19 100644 --- a/app/src/main/java/org/tasks/activities/GoogleTaskListSettingsActivity.kt +++ b/app/src/main/java/org/tasks/activities/GoogleTaskListSettingsActivity.kt @@ -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 } diff --git a/app/src/main/java/org/tasks/activities/PlaceSettingsActivity.kt b/app/src/main/java/org/tasks/activities/PlaceSettingsActivity.kt index ac5333ce0..83bb24955 100644 --- a/app/src/main/java/org/tasks/activities/PlaceSettingsActivity.kt +++ b/app/src/main/java/org/tasks/activities/PlaceSettingsActivity.kt @@ -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( - R.string.location_radius_meters, - locale.formatNumber(value.toInt()) - ) + 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(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 diff --git a/app/src/main/java/org/tasks/activities/TagSettingsActivity.kt b/app/src/main/java/org/tasks/activities/TagSettingsActivity.kt index 856f8a663..03732466e 100644 --- a/app/src/main/java/org/tasks/activities/TagSettingsActivity.kt +++ b/app/src/main/java/org/tasks/activities/TagSettingsActivity.kt @@ -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 } - name.setText(tagData.name) - if (isNewTag) { - name.requestFocus() - val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.showSoftInput(name, InputMethodManager.SHOW_IMPLICIT) + + setContent { + TasksTheme { + baseSettingsContent() + } } 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) @@ -158,4 +143,5 @@ class TagSettingsActivity : BaseListSettingsActivity() { const val EXTRA_TAG_DATA = "tagData" // $NON-NLS-1$ private const val EXTRA_TAG_UUID = "uuid" // $NON-NLS-1$ } -} \ No newline at end of file +} + diff --git a/app/src/main/java/org/tasks/caldav/BaseCaldavCalendarSettingsActivity.kt b/app/src/main/java/org/tasks/caldav/BaseCaldavCalendarSettingsActivity.kt index 0f8c5995c..96ff03c06 100644 --- a/app/src/main/java/org/tasks/caldav/BaseCaldavCalendarSettingsActivity.kt +++ b/app/src/main/java/org/tasks/caldav/BaseCaldavCalendarSettingsActivity.kt @@ -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,16 +50,11 @@ 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" diff --git a/app/src/main/java/org/tasks/caldav/CaldavCalendarSettingsActivity.kt b/app/src/main/java/org/tasks/caldav/CaldavCalendarSettingsActivity.kt index e64a7d961..949e6b4c9 100644 --- a/app/src/main/java/org/tasks/caldav/CaldavCalendarSettingsActivity.kt +++ b/app/src/main/java/org/tasks/caldav/CaldavCalendarSettingsActivity.kt @@ -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> = mutableStateOf( emptyList().toMutableList()) + private val removeDialog = mutableStateOf(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(R.id.people).setContent { - TasksTheme(theme = tasksTheme.themeBase.index) { - val principals = principalDao.getPrincipals(it.id).collectAsStateWithLifecycle(initialValue = emptyList()).value - PrincipalList( - principals = principals, - onRemove = if (canRemovePrincipals) { { onRemove(it) } } else null, - ) + 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 + ) + } + ) + } + } - } - } - if (caldavAccount.canShare && (isNew || caldavCalendar?.access == ACCESS_OWNER)) { - findViewById(R.id.fab) - .apply { isVisible = true } - .setContent { - TasksTheme(theme = tasksTheme.themeBase.index) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.BottomEnd + ) { + if (caldavAccount.canShare && (isNew || caldavCalendar?.access == ACCESS_OWNER)) { val openDialog = rememberSaveable { mutableStateOf(false) } ShareInviteDialog( openDialog, @@ -79,16 +133,18 @@ 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, ) } } } + } } } @@ -96,15 +152,8 @@ class CaldavCalendarSettingsActivity : BaseCaldavCalendarSettingsActivity() { 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 { diff --git a/app/src/main/java/org/tasks/caldav/LocalListSettingsActivity.kt b/app/src/main/java/org/tasks/caldav/LocalListSettingsActivity.kt index d9e2b54e1..0bd09b66f 100644 --- a/app/src/main/java/org/tasks/caldav/LocalListSettingsActivity.kt +++ b/app/src/main/java/org/tasks/caldav/LocalListSettingsActivity.kt @@ -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) = diff --git a/app/src/main/java/org/tasks/compose/DragSortList.kt b/app/src/main/java/org/tasks/compose/DragSortList.kt new file mode 100644 index 000000000..29f1aaa21 --- /dev/null +++ b/app/src/main/java/org/tasks/compose/DragSortList.kt @@ -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(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( 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) + } +} diff --git a/app/src/main/java/org/tasks/compose/FilterCondition.kt b/app/src/main/java/org/tasks/compose/FilterCondition.kt new file mode 100644 index 000000000..36d14884b --- /dev/null +++ b/app/src/main/java/org/tasks/compose/FilterCondition.kt @@ -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, + 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, + 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, + 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, + 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, + 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 */ +} \ No newline at end of file diff --git a/app/src/main/java/org/tasks/compose/ListSettings.kt b/app/src/main/java/org/tasks/compose/ListSettings.kt new file mode 100644 index 000000000..687c380e7 --- /dev/null +++ b/app/src/main/java/org/tasks/compose/ListSettings.kt @@ -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) { + 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, + error: MutableState, + 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, 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, + 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) } + ) + } + } + +} diff --git a/app/src/main/java/org/tasks/compose/SwipeOut.kt b/app/src/main/java/org/tasks/compose/SwipeOut.kt new file mode 100644 index 000000000..f0e5713df --- /dev/null +++ b/app/src/main/java/org/tasks/compose/SwipeOut.kt @@ -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 = 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() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/tasks/etebase/EtebaseCalendarSettingsActivity.kt b/app/src/main/java/org/tasks/etebase/EtebaseCalendarSettingsActivity.kt index 5e9ffe722..42882b38e 100644 --- a/app/src/main/java/org/tasks/etebase/EtebaseCalendarSettingsActivity.kt +++ b/app/src/main/java/org/tasks/etebase/EtebaseCalendarSettingsActivity.kt @@ -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) = diff --git a/app/src/main/java/org/tasks/location/MapFragment.kt b/app/src/main/java/org/tasks/location/MapFragment.kt index 79a45592e..a0fa16599 100644 --- a/app/src/main/java/org/tasks/location/MapFragment.kt +++ b/app/src/main/java/org/tasks/location/MapFragment.kt @@ -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? diff --git a/app/src/main/java/org/tasks/location/OsmMapFragment.kt b/app/src/main/java/org/tasks/location/OsmMapFragment.kt index 399846851..b1564284c 100644 --- a/app/src/main/java/org/tasks/location/OsmMapFragment.kt +++ b/app/src/main/java/org/tasks/location/OsmMapFragment.kt @@ -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(R.id.map).addView(this) + if (parent != null) parent.addView(this) + else activity.findViewById(R.id.map).addView(this) } callback.onMapReady(this) } diff --git a/app/src/main/java/org/tasks/opentasks/OpenTasksListSettingsActivity.kt b/app/src/main/java/org/tasks/opentasks/OpenTasksListSettingsActivity.kt index 62449ebf8..32f079bc9 100644 --- a/app/src/main/java/org/tasks/opentasks/OpenTasksListSettingsActivity.kt +++ b/app/src/main/java/org/tasks/opentasks/OpenTasksListSettingsActivity.kt @@ -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,15 +21,26 @@ 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) {} override suspend fun updateNameAndColor( - account: CaldavAccount, calendar: CaldavCalendar, name: String, color: Int) = + account: CaldavAccount, calendar: CaldavCalendar, name: String, color: Int) = updateCalendar() override suspend fun deleteCalendar(caldavAccount: CaldavAccount, caldavCalendar: CaldavCalendar) {} diff --git a/app/src/main/java/org/tasks/sync/microsoft/MicrosoftListSettingsActivity.kt b/app/src/main/java/org/tasks/sync/microsoft/MicrosoftListSettingsActivity.kt index c69bc7cbe..06f63cf71 100644 --- a/app/src/main/java/org/tasks/sync/microsoft/MicrosoftListSettingsActivity.kt +++ b/app/src/main/java/org/tasks/sync/microsoft/MicrosoftListSettingsActivity.kt @@ -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) = diff --git a/app/src/main/res/layout/filter_settings_activity.xml b/app/src/main/res/layout/filter_settings_activity.xml index a2d7e4fe5..eef72425d 100644 --- a/app/src/main/res/layout/filter_settings_activity.xml +++ b/app/src/main/res/layout/filter_settings_activity.xml @@ -57,6 +57,11 @@ android:clipToPadding="false" android:nestedScrollingEnabled="false"/> + + diff --git a/kmp/src/commonMain/kotlin/org/tasks/themes/TasksTheme.kt b/kmp/src/commonMain/kotlin/org/tasks/themes/TasksTheme.kt index b38bc0a03..eb09fdd0d 100644 --- a/kmp/src/commonMain/kotlin/org/tasks/themes/TasksTheme.kt +++ b/kmp/src/commonMain/kotlin/org/tasks/themes/TasksTheme.kt @@ -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()