mirror of https://github.com/tasks/tasks
Convert list settings to compose (#3163)
parent
87c5ec9f14
commit
8e5b93fc10
@ -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"
|
||||||
|
}
|
||||||
@ -0,0 +1,199 @@
|
|||||||
|
package org.tasks.compose
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.FastOutLinearInEasing
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.gestures.animateScrollBy
|
||||||
|
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.lazy.LazyItemScope
|
||||||
|
import androidx.compose.foundation.lazy.LazyListItemInfo
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drag - drop to reorder elements of LazyColumn
|
||||||
|
*
|
||||||
|
* Implementation is based on:
|
||||||
|
* https://github.com/realityexpander/DragDropColumnCompose
|
||||||
|
*
|
||||||
|
* Scheme of use:
|
||||||
|
* 1. Hoist state of the LazyColumn (create your own and set it as LazyColumn parameter)
|
||||||
|
* 2. Create and remember DragDropState object by call to "rememberDragDropState"
|
||||||
|
* 3. Use Modifier.doDrag in the LazyColumn
|
||||||
|
* 4. enclose LazyList items into DraggableItem
|
||||||
|
* **/
|
||||||
|
|
||||||
|
class DragDropState internal constructor(
|
||||||
|
val state: LazyListState,
|
||||||
|
private val scope: CoroutineScope,
|
||||||
|
private val confirmDrag: (Int) -> Boolean,
|
||||||
|
private val complete: () -> Unit,
|
||||||
|
private val swap: (Int, Int) -> Unit,
|
||||||
|
) {
|
||||||
|
/* primary ID of the item being dragged */
|
||||||
|
var draggedItemIndex by mutableStateOf<Int?>(null)
|
||||||
|
|
||||||
|
private var draggedDistance by mutableFloatStateOf(0f)
|
||||||
|
private var draggingElementOffset: Int = 0 // cached drugged element offset and size
|
||||||
|
private var draggingElementSize: Int = -1 // size must not be negative when dragging is in progress
|
||||||
|
|
||||||
|
private var overscrollJob by mutableStateOf<Job?>( null )
|
||||||
|
|
||||||
|
/* sibling of draggingElementOffset, not cached, for use in animation */
|
||||||
|
internal val draggingItemOffset: Float
|
||||||
|
get() = state.layoutInfo.visibleItemsInfo
|
||||||
|
.firstOrNull { it.index == draggedItemIndex }
|
||||||
|
?.let { item ->
|
||||||
|
draggingElementOffset + draggedDistance - item.offset
|
||||||
|
} ?: 0f
|
||||||
|
|
||||||
|
fun itemAtOffset(offsetY: Float): LazyListItemInfo? =
|
||||||
|
state.layoutInfo.visibleItemsInfo.firstOrNull {
|
||||||
|
item -> offsetY.toInt() in item.offset..(item.offset + item.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startDragging(item: LazyListItemInfo?) {
|
||||||
|
//Log.d("HADY", "start dragging ${item}")
|
||||||
|
if (item != null && confirmDrag(item.index)) {
|
||||||
|
draggedItemIndex = item.index
|
||||||
|
draggingElementOffset = item.offset
|
||||||
|
draggingElementSize = item.size
|
||||||
|
assert(item.size >= 0) { "Invalid size of element ${item.size}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopDragging() {
|
||||||
|
draggedDistance = 0f
|
||||||
|
draggedItemIndex = null
|
||||||
|
draggingElementOffset = 0
|
||||||
|
draggingElementSize = -1
|
||||||
|
overscrollJob?.cancel()
|
||||||
|
complete()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onDrag(offset: Offset) {
|
||||||
|
|
||||||
|
draggedDistance += offset.y
|
||||||
|
|
||||||
|
if (draggedItemIndex != null) {
|
||||||
|
assert(draggingElementSize >= 0) { "FATAL: Invalid dragging element" }
|
||||||
|
|
||||||
|
val startOffset = draggingElementOffset + draggedDistance
|
||||||
|
val endOffset = startOffset + draggingElementSize
|
||||||
|
|
||||||
|
val draggedIndex = draggedItemIndex
|
||||||
|
val dragged = draggedIndex?.let { index ->
|
||||||
|
state.layoutInfo.visibleItemsInfo.getOrNull(
|
||||||
|
index - state.layoutInfo.visibleItemsInfo.first().index
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (dragged != null) {
|
||||||
|
val up = (startOffset - dragged.offset) > 0
|
||||||
|
val hovered =
|
||||||
|
if (up) itemAtOffset(startOffset + 0.1f * draggingElementSize)
|
||||||
|
else itemAtOffset(endOffset - 0.1f * draggingElementSize)
|
||||||
|
|
||||||
|
if (hovered != null) {
|
||||||
|
scope.launch { swap(draggedIndex, hovered.index) }
|
||||||
|
draggedItemIndex = hovered.index
|
||||||
|
}
|
||||||
|
|
||||||
|
if (overscrollJob?.isActive != true) {
|
||||||
|
val overscroll = when {
|
||||||
|
draggedDistance > 0 -> max(endOffset - state.layoutInfo.viewportEndOffset+50f, 0f)
|
||||||
|
draggedDistance < 0 -> min(startOffset - state.layoutInfo.viewportStartOffset-50f, 0f)
|
||||||
|
else -> 0f
|
||||||
|
}
|
||||||
|
if (overscroll != 0f) {
|
||||||
|
overscrollJob = scope.launch {
|
||||||
|
state.animateScrollBy(
|
||||||
|
overscroll * 1.3f, tween(easing = FastOutLinearInEasing)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} /* end onDrag */
|
||||||
|
} /* end DragDropState */
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun rememberDragDropState(
|
||||||
|
lazyListState: LazyListState,
|
||||||
|
confirmDrag: (Int) -> Boolean = { true },
|
||||||
|
completeDragDrop: () -> Unit,
|
||||||
|
doSwap: (Int, Int) -> Unit
|
||||||
|
): DragDropState {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val state = remember(lazyListState) {
|
||||||
|
DragDropState(
|
||||||
|
state = lazyListState,
|
||||||
|
swap = doSwap,
|
||||||
|
complete = completeDragDrop,
|
||||||
|
scope = scope,
|
||||||
|
confirmDrag = confirmDrag
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Modifier.doDrag(dragDropState: DragDropState): Modifier =
|
||||||
|
this.pointerInput(dragDropState) {
|
||||||
|
detectDragGesturesAfterLongPress(
|
||||||
|
onDragStart = { offset ->
|
||||||
|
dragDropState.startDragging(dragDropState.itemAtOffset(offset.y))
|
||||||
|
},
|
||||||
|
onDrag = { change, offset ->
|
||||||
|
change.consume()
|
||||||
|
dragDropState.onDrag(offset)
|
||||||
|
},
|
||||||
|
onDragEnd = { dragDropState.stopDragging() },
|
||||||
|
onDragCancel = { dragDropState.stopDragging() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalFoundationApi
|
||||||
|
@Composable
|
||||||
|
fun LazyItemScope.DraggableItem(
|
||||||
|
dragDropState: DragDropState,
|
||||||
|
index: Int,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable (isDragging: Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
val current: Float by animateFloatAsState(
|
||||||
|
targetValue = dragDropState.draggingItemOffset * 0.67f
|
||||||
|
)
|
||||||
|
|
||||||
|
val dragging = index == dragDropState.draggedItemIndex
|
||||||
|
|
||||||
|
val draggingModifier = if (dragging) {
|
||||||
|
Modifier
|
||||||
|
.zIndex(1f)
|
||||||
|
.graphicsLayer { translationY = current }
|
||||||
|
} else {
|
||||||
|
Modifier.animateItemPlacement(
|
||||||
|
tween(easing = FastOutLinearInEasing)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(modifier = modifier.then(draggingModifier)) {
|
||||||
|
content(dragging)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,496 @@
|
|||||||
|
package org.tasks.compose
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composables for FilterSettingActivity
|
||||||
|
**/
|
||||||
|
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Abc
|
||||||
|
import androidx.compose.material.icons.outlined.Add
|
||||||
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.FloatingActionButton
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.MutableIntState
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.colorResource
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.core.os.ConfigurationCompat
|
||||||
|
import com.todoroo.astrid.core.CriterionInstance
|
||||||
|
import org.tasks.R
|
||||||
|
import org.tasks.compose.ListSettings.SettingRow
|
||||||
|
import org.tasks.compose.SwipeOut.SwipeOut
|
||||||
|
import org.tasks.extensions.formatNumber
|
||||||
|
import org.tasks.themes.TasksTheme
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview (showBackground = true)
|
||||||
|
private fun CriterionTypeSelectPreview () {
|
||||||
|
TasksTheme {
|
||||||
|
FilterCondition.SelectCriterionType(
|
||||||
|
title = "Select criterion type",
|
||||||
|
selected = 1,
|
||||||
|
types = listOf("AND", "OR", "NOT"),
|
||||||
|
onCancel = { /*TODO*/ }) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview (showBackground = true)
|
||||||
|
private fun InputTextPreview () {
|
||||||
|
TasksTheme {
|
||||||
|
FilterCondition.InputTextOption(title = "Task name contains...", onCancel = { /*TODO*/ }
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview (showBackground = true)
|
||||||
|
private fun SwipeOutDecorationPreview () {
|
||||||
|
TasksTheme {
|
||||||
|
Box(modifier = Modifier
|
||||||
|
.height(56.dp)
|
||||||
|
.fillMaxWidth()) {
|
||||||
|
FilterCondition.SwipeOutDecoration()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview (showBackground = true)
|
||||||
|
private fun FabPreview () {
|
||||||
|
TasksTheme {
|
||||||
|
FilterCondition.NewCriterionFAB(
|
||||||
|
isExtended = remember { mutableStateOf(true) }
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object FilterCondition {
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun FilterCondition(
|
||||||
|
items: SnapshotStateList<CriterionInstance>,
|
||||||
|
onDelete: (Int) -> Unit,
|
||||||
|
doSwap: (Int, Int) -> Unit,
|
||||||
|
onComplete: () -> Unit,
|
||||||
|
onClick: (String) -> Unit
|
||||||
|
) {
|
||||||
|
|
||||||
|
val getIcon: (CriterionInstance) -> Int = { criterion ->
|
||||||
|
when (criterion.type) {
|
||||||
|
CriterionInstance.TYPE_ADD -> R.drawable.ic_call_split_24px
|
||||||
|
CriterionInstance.TYPE_SUBTRACT -> R.drawable.ic_outline_not_interested_24px
|
||||||
|
CriterionInstance.TYPE_INTERSECT -> R.drawable.ic_outline_add_24px
|
||||||
|
else -> {
|
||||||
|
0
|
||||||
|
} /* assert */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val listState = rememberLazyListState()
|
||||||
|
val dragDropState = rememberDragDropState(
|
||||||
|
lazyListState = listState,
|
||||||
|
confirmDrag = { index -> index != 0 },
|
||||||
|
completeDragDrop = onComplete,
|
||||||
|
) { fromIndex, toIndex ->
|
||||||
|
if (fromIndex != 0 && toIndex != 0) doSwap(fromIndex, toIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.custom_filter_criteria),
|
||||||
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(Constants.KEYLINE_FIRST)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Row {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.doDrag(dragDropState),
|
||||||
|
userScrollEnabled = true,
|
||||||
|
state = listState
|
||||||
|
) {
|
||||||
|
itemsIndexed(
|
||||||
|
items = items,
|
||||||
|
key = { _, item -> item.id + " " + item.type + " " + item.end}
|
||||||
|
) { index, criterion ->
|
||||||
|
if (index == 0) {
|
||||||
|
FilterConditionRow(criterion, false, getIcon, onClick)
|
||||||
|
} else {
|
||||||
|
DraggableItem(
|
||||||
|
dragDropState = dragDropState, index = index
|
||||||
|
) { dragging ->
|
||||||
|
SwipeOut(
|
||||||
|
decoration = { SwipeOutDecoration() },
|
||||||
|
onSwipe = { index -> onDelete(index) },
|
||||||
|
index = index
|
||||||
|
) {
|
||||||
|
FilterConditionRow(criterion, dragging, getIcon, onClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} /* FilterCondition */
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun FilterConditionRow(
|
||||||
|
criterion: CriterionInstance,
|
||||||
|
dragging: Boolean,
|
||||||
|
getIcon: (CriterionInstance) -> Int,
|
||||||
|
onClick: (String) -> Unit
|
||||||
|
) {
|
||||||
|
HorizontalDivider(
|
||||||
|
color = when (criterion.type) {
|
||||||
|
CriterionInstance.TYPE_ADD -> Color.Gray
|
||||||
|
else -> Color.Transparent
|
||||||
|
}
|
||||||
|
)
|
||||||
|
val modifier =
|
||||||
|
if (dragging) Modifier.background(Color.LightGray)
|
||||||
|
else Modifier
|
||||||
|
SettingRow(
|
||||||
|
modifier = modifier.clickable { onClick(criterion.id) },
|
||||||
|
left = {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.size(56.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (criterion.type != CriterionInstance.TYPE_UNIVERSE) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.padding(Constants.KEYLINE_FIRST),
|
||||||
|
painter = painterResource(id = getIcon(criterion)),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
center = {
|
||||||
|
Text(
|
||||||
|
text = criterion.titleFromCriterion,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.padding(start = 16.dp, top = 16.dp, bottom = 16.dp)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
right = {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val locale = remember {
|
||||||
|
ConfigurationCompat
|
||||||
|
.getLocales(context.resources.configuration)
|
||||||
|
.get(0)
|
||||||
|
?: Locale.getDefault()
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = locale.formatNumber(criterion.end),
|
||||||
|
modifier = Modifier.padding(end = Constants.KEYLINE_FIRST),
|
||||||
|
color = Color.Gray,
|
||||||
|
fontSize = 14.sp,
|
||||||
|
textAlign = TextAlign.End
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SwipeOutDecoration() {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(colorResource(id = org.tasks.kmp.R.color.red_a400))
|
||||||
|
//.background(MaterialTheme.colorScheme.secondary)
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun deleteIcon() {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.padding(horizontal = Constants.KEYLINE_FIRST),
|
||||||
|
imageVector = Icons.Outlined.Delete,
|
||||||
|
contentDescription = "Delete",
|
||||||
|
tint = Color.White.copy(alpha = 0.6f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.height(56.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
deleteIcon()
|
||||||
|
deleteIcon()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} /* end SwipeOutDecoration */
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NewCriterionFAB(
|
||||||
|
isExtended: MutableState<Boolean>,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
|
||||||
|
Box( // lays out over main content as a space to layout FAB
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.BottomEnd
|
||||||
|
) {
|
||||||
|
FloatingActionButton(
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
shape = RoundedCornerShape(50),
|
||||||
|
containerColor = MaterialTheme.colorScheme.secondary,
|
||||||
|
contentColor = Color.White,
|
||||||
|
) {
|
||||||
|
val extended = isExtended.value
|
||||||
|
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Add,
|
||||||
|
contentDescription = "New Criteria",
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
start = if (extended) 16.dp else 0.dp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (extended)
|
||||||
|
Text(
|
||||||
|
text = LocalContext.current.getString(R.string.CFA_button_add),
|
||||||
|
modifier = Modifier.padding(end = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} /* end FloatingActionButton */
|
||||||
|
}
|
||||||
|
} /* end NewCriterionFAB */
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SelectCriterionType(
|
||||||
|
title: String,
|
||||||
|
selected: Int,
|
||||||
|
types: List<String>,
|
||||||
|
onCancel: () -> Unit,
|
||||||
|
help: () -> Unit = {},
|
||||||
|
onSelected: (Int) -> Unit
|
||||||
|
) {
|
||||||
|
val selected = remember { mutableIntStateOf(selected) }
|
||||||
|
|
||||||
|
Dialog(onDismissRequest = onCancel)
|
||||||
|
{
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.background)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier
|
||||||
|
.padding(horizontal = Constants.KEYLINE_FIRST)
|
||||||
|
.padding(top = Constants.HALF_KEYLINE)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(48.dp)
|
||||||
|
.padding(top = 16.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(Constants.HALF_KEYLINE))
|
||||||
|
ToggleGroup(items = types, selected = selected)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.height(48.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Box(contentAlignment = Alignment.CenterStart) {
|
||||||
|
Constants.TextButton(text = R.string.help, onClick = help)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.CenterEnd,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
Constants.TextButton(text = R.string.cancel, onClick = onCancel)
|
||||||
|
Constants.TextButton(text = R.string.ok) { onSelected(selected.intValue) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} /* end SelectCriterionType */
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ToggleGroup(
|
||||||
|
items: List<String>,
|
||||||
|
selected: MutableIntState = remember { mutableIntStateOf(0) }
|
||||||
|
) {
|
||||||
|
assert(selected.intValue in items.indices)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(56.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
for (index in items.indices) {
|
||||||
|
val highlight = (index == selected.intValue)
|
||||||
|
val color =
|
||||||
|
if (highlight) MaterialTheme.colorScheme.secondary.copy(alpha = 0.5f)
|
||||||
|
else MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f)
|
||||||
|
OutlinedButton(
|
||||||
|
onClick = { selected.intValue = index },
|
||||||
|
border = BorderStroke(1.dp, SolidColor(color.copy(alpha = 0.5f))),
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(
|
||||||
|
containerColor = color.copy(alpha = 0.2f),
|
||||||
|
contentColor = MaterialTheme.colorScheme.onBackground),
|
||||||
|
shape = RoundedCornerShape(Constants.HALF_KEYLINE)
|
||||||
|
) {
|
||||||
|
Text(items[index])
|
||||||
|
}
|
||||||
|
if (index<items.size-1) Spacer(modifier = Modifier.size(2.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} /* end ToggleGroup */
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SelectFromList(
|
||||||
|
names: List<String>,
|
||||||
|
title: String? = null,
|
||||||
|
onCancel: () -> Unit,
|
||||||
|
onSelected: (Int) -> Unit
|
||||||
|
) {
|
||||||
|
Dialog(onDismissRequest = onCancel) {
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.background)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = Constants.KEYLINE_FIRST)
|
||||||
|
.padding(bottom = Constants.KEYLINE_FIRST)
|
||||||
|
) {
|
||||||
|
title?.let { title ->
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(48.dp)
|
||||||
|
.padding(top = Constants.KEYLINE_FIRST)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
names.forEachIndexed { index, name ->
|
||||||
|
Text(
|
||||||
|
text = name,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(48.dp)
|
||||||
|
.padding(top = Constants.KEYLINE_FIRST)
|
||||||
|
.clickable { onSelected(index) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} /* end SelectFromList */
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun InputTextOption(
|
||||||
|
title: String,
|
||||||
|
onCancel: () -> Unit,
|
||||||
|
onDone: (String) -> Unit
|
||||||
|
) {
|
||||||
|
val text = remember { mutableStateOf("") }
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onCancel,
|
||||||
|
confirmButton = {
|
||||||
|
Constants.TextButton(
|
||||||
|
text = R.string.ok,
|
||||||
|
onClick = { onDone(text.value) })
|
||||||
|
},
|
||||||
|
dismissButton = { Constants.TextButton(text = R.string.cancel, onClick = onCancel) },
|
||||||
|
text = {
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(Constants.KEYLINE_FIRST))
|
||||||
|
OutlinedTextField(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
value = text.value,
|
||||||
|
label = { Text(title) },
|
||||||
|
onValueChange = { text.value = it },
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Abc,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
textStyle = MaterialTheme.typography.bodyMedium,
|
||||||
|
colors = Constants.textFieldColors(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} /* end InputTextOption */
|
||||||
|
}
|
||||||
@ -0,0 +1,374 @@
|
|||||||
|
package org.tasks.compose
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Composables for BaseListSettingActivity
|
||||||
|
*/
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.requiredHeight
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ProvideTextStyle
|
||||||
|
import androidx.compose.material3.Snackbar
|
||||||
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
|
import androidx.compose.ui.res.colorResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.res.vectorResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import org.tasks.R
|
||||||
|
import org.tasks.compose.components.TasksIcon
|
||||||
|
import org.tasks.themes.TasksIcons
|
||||||
|
import org.tasks.themes.TasksTheme
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview (showBackground = true)
|
||||||
|
private fun TitleBarPreview() {
|
||||||
|
TasksTheme {
|
||||||
|
ListSettings.Toolbar(
|
||||||
|
title = "Tollbar title",
|
||||||
|
save = { /*TODO*/ }, optionButton = { DeleteButton {} }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
private fun PromptActionPreview() {
|
||||||
|
TasksTheme {
|
||||||
|
ListSettings.PromptAction(
|
||||||
|
showDialog = remember { mutableStateOf(true) },
|
||||||
|
title = "Delete list?",
|
||||||
|
onAction = { /*TODO*/ })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview (showBackground = true)
|
||||||
|
private fun IconSelectPreview () {
|
||||||
|
TasksTheme {
|
||||||
|
ListSettings.SelectIconRow(
|
||||||
|
icon = TasksIcons.FILTER_LIST,
|
||||||
|
selectIcon = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview (showBackground = true)
|
||||||
|
private fun ColorSelectPreview () {
|
||||||
|
TasksTheme {
|
||||||
|
ListSettings.SelectColorRow(
|
||||||
|
color = remember { mutableStateOf(Color.Red) },
|
||||||
|
selectColor = {},
|
||||||
|
clearColor = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object ListSettings {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Toolbar(
|
||||||
|
title: String,
|
||||||
|
save: () -> Unit,
|
||||||
|
optionButton: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
|
||||||
|
/* Hady: reminder for the future
|
||||||
|
val activity = LocalView.current.context as Activity
|
||||||
|
activity.window.statusBarColor = colorResource(id = R.color.drawer_color_selected).toArgb()
|
||||||
|
*/
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
shadowElevation = 4.dp,
|
||||||
|
color = colorResource(id = R.color.content_background),
|
||||||
|
contentColor = colorResource(id = R.color.text_primary),
|
||||||
|
modifier = Modifier.requiredHeight(56.dp)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
IconButton(onClick = save, modifier = Modifier.size(56.dp)) {
|
||||||
|
Icon(
|
||||||
|
imageVector = ImageVector.vectorResource(R.drawable.ic_outline_save_24px),
|
||||||
|
contentDescription = stringResource(id = R.string.save),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
fontSize = 20.sp,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(0.9f)
|
||||||
|
.padding(start = Constants.KEYLINE_FIRST)
|
||||||
|
)
|
||||||
|
optionButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} /* ToolBar */
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ProgressBar(showProgress: State<Boolean>) {
|
||||||
|
Box(modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.requiredHeight(3.dp))
|
||||||
|
{
|
||||||
|
if (showProgress.value) {
|
||||||
|
LinearProgressIndicator(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
trackColor = LocalContentColor.current.copy(alpha = 0.3f), //Color.LightGray,
|
||||||
|
color = colorResource(org.tasks.kmp.R.color.red_a400)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TitleInput(
|
||||||
|
text: MutableState<String>,
|
||||||
|
error: MutableState<String>,
|
||||||
|
requestKeyboard: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
label: String = stringResource(R.string.display_name),
|
||||||
|
errorState: Color = MaterialTheme.colorScheme.secondary,
|
||||||
|
activeState: Color = LocalContentColor.current.copy(alpha = 0.75f),
|
||||||
|
inactiveState: Color = LocalContentColor.current.copy(alpha = 0.3f),
|
||||||
|
) {
|
||||||
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
val requester = remember { FocusRequester() }
|
||||||
|
val focused = remember { mutableStateOf(false) }
|
||||||
|
val labelColor = when {
|
||||||
|
(error.value != "") -> errorState
|
||||||
|
(focused.value) -> activeState
|
||||||
|
else -> inactiveState
|
||||||
|
}
|
||||||
|
val dividerColor = if (focused.value) errorState else labelColor
|
||||||
|
val labelText = if (error.value != "") error.value else label
|
||||||
|
|
||||||
|
Row (modifier = modifier)
|
||||||
|
{
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(top = 18.dp, bottom = 4.dp),
|
||||||
|
text = labelText,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
letterSpacing = 0.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = labelColor
|
||||||
|
)
|
||||||
|
|
||||||
|
BasicTextField(
|
||||||
|
value = text.value,
|
||||||
|
textStyle = TextStyle(
|
||||||
|
fontSize = LocalTextStyle.current.fontSize,
|
||||||
|
color = LocalContentColor.current
|
||||||
|
),
|
||||||
|
onValueChange = {
|
||||||
|
text.value = it
|
||||||
|
if (error.value != "") error.value = ""
|
||||||
|
},
|
||||||
|
cursorBrush = SolidColor(errorState), // SolidColor(LocalContentColor.current),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 3.dp)
|
||||||
|
.focusRequester(requester)
|
||||||
|
.onFocusChanged { focused.value = (it.isFocused) }
|
||||||
|
)
|
||||||
|
HorizontalDivider(
|
||||||
|
color = dividerColor,
|
||||||
|
modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestKeyboard) {
|
||||||
|
LaunchedEffect(null) {
|
||||||
|
requester.requestFocus()
|
||||||
|
delay(30) // Workaround. Otherwise keyboard don't show in 4/5 tries
|
||||||
|
keyboardController?.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} /* TextInput */
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SelectColorRow(color: State<Color>, selectColor: () -> Unit, clearColor: () -> Unit) =
|
||||||
|
SettingRow(
|
||||||
|
modifier = Modifier.clickable(onClick = selectColor),
|
||||||
|
left = {
|
||||||
|
IconButton(onClick = { selectColor() }) {
|
||||||
|
if (color.value == Color.Unspecified) {
|
||||||
|
Icon(
|
||||||
|
imageVector = ImageVector.vectorResource(R.drawable.ic_outline_not_interested_24px),
|
||||||
|
tint = colorResource(R.color.icon_tint_with_alpha),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val borderColor = colorResource(R.color.icon_tint_with_alpha) // colorResource(R.color.text_tertiary)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.size(56.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Canvas(modifier = Modifier.size(24.dp)) {
|
||||||
|
drawCircle(color = color.value)
|
||||||
|
drawCircle(color = borderColor, style = Stroke(width = 4.0f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
center = {
|
||||||
|
Text(
|
||||||
|
text = LocalContext.current.getString(R.string.color),
|
||||||
|
modifier = Modifier.padding(start = Constants.KEYLINE_FIRST)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
right = {
|
||||||
|
if (color.value != Color.Unspecified) {
|
||||||
|
IconButton(onClick = clearColor) {
|
||||||
|
Icon(
|
||||||
|
imageVector = ImageVector.vectorResource(id = R.drawable.ic_outline_clear_24px),
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SelectIconRow(icon: String, selectIcon: () -> Unit) =
|
||||||
|
SettingRow(
|
||||||
|
modifier = Modifier.clickable(onClick = selectIcon),
|
||||||
|
left = {
|
||||||
|
IconButton(onClick = selectIcon) {
|
||||||
|
TasksIcon(
|
||||||
|
label = icon,
|
||||||
|
tint = colorResource(R.color.icon_tint_with_alpha)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
center = {
|
||||||
|
Text(
|
||||||
|
text = LocalContext.current.getString(R.string.icon),
|
||||||
|
modifier = Modifier.padding(start = Constants.KEYLINE_FIRST)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingRow(
|
||||||
|
left: @Composable () -> Unit,
|
||||||
|
center: @Composable () -> Unit,
|
||||||
|
right: @Composable (() -> Unit)? = null,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) {
|
||||||
|
Box (modifier = Modifier.size(56.dp), contentAlignment = Alignment.Center) {
|
||||||
|
left()
|
||||||
|
}
|
||||||
|
Box (
|
||||||
|
modifier = Modifier
|
||||||
|
.height(56.dp)
|
||||||
|
.weight(1f), contentAlignment = Alignment.CenterStart
|
||||||
|
) {
|
||||||
|
center()
|
||||||
|
}
|
||||||
|
right?.let {
|
||||||
|
Box (modifier = Modifier.size(56.dp), contentAlignment = Alignment.Center) {
|
||||||
|
it.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingsSurface(content: @Composable ColumnScope.() -> Unit) {
|
||||||
|
ProvideTextStyle(LocalTextStyle.current.copy(fontSize = 18.sp)) {
|
||||||
|
Surface(
|
||||||
|
color = colorResource(id = R.color.window_background),
|
||||||
|
contentColor = colorResource(id = R.color.text_primary)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.fillMaxSize()) { content() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Toaster(state: SnackbarHostState) {
|
||||||
|
SnackbarHost(state) { data ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Snackbar(
|
||||||
|
modifier = Modifier.padding(horizontal = 24.dp),
|
||||||
|
shape = RoundedCornerShape(10.dp),
|
||||||
|
containerColor = colorResource(id = R.color.snackbar_background),
|
||||||
|
contentColor = colorResource(id = R.color.snackbar_text_color),
|
||||||
|
) {
|
||||||
|
Text(text = data.visuals.message, fontSize = 18.sp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PromptAction(
|
||||||
|
showDialog: MutableState<Boolean>,
|
||||||
|
title: String,
|
||||||
|
onAction: () -> Unit,
|
||||||
|
onCancel: () -> Unit = { showDialog.value = false }
|
||||||
|
) {
|
||||||
|
if (showDialog.value) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onCancel,
|
||||||
|
title = { Text(title, style = MaterialTheme.typography.headlineSmall) },
|
||||||
|
confirmButton = { Constants.TextButton(text = R.string.ok, onClick = onAction) },
|
||||||
|
dismissButton = { Constants.TextButton(text = R.string.cancel, onClick = onCancel) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,94 @@
|
|||||||
|
package org.tasks.compose
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple Swipe-to-delete implementation
|
||||||
|
*/
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.exponentialDecay
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.gestures.AnchoredDraggableState
|
||||||
|
import androidx.compose.foundation.gestures.DraggableAnchors
|
||||||
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
|
import androidx.compose.foundation.gestures.anchoredDraggable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.res.colorResource
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.tasks.R
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
object SwipeOut {
|
||||||
|
|
||||||
|
private enum class Anchors { Left, Center, Right }
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun SwipeOut(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
index: Int,
|
||||||
|
onSwipe: (Int) -> Unit,
|
||||||
|
decoration: @Composable BoxScope.() -> Unit = {},
|
||||||
|
content: @Composable BoxScope.() -> Unit
|
||||||
|
) {
|
||||||
|
val screenWidthPx =
|
||||||
|
with(LocalDensity.current) {
|
||||||
|
LocalConfiguration.current.screenWidthDp.dp.roundToPx().toFloat()
|
||||||
|
}
|
||||||
|
|
||||||
|
val dragState: AnchoredDraggableState<Anchors> = remember {
|
||||||
|
AnchoredDraggableState(
|
||||||
|
initialValue = Anchors.Center,
|
||||||
|
anchors = DraggableAnchors {
|
||||||
|
Anchors.Left at -screenWidthPx * 3/4
|
||||||
|
Anchors.Center at 0f
|
||||||
|
Anchors.Right at screenWidthPx * 3/4
|
||||||
|
},
|
||||||
|
positionalThreshold = { _ -> screenWidthPx/3 },
|
||||||
|
velocityThreshold = { 100f },
|
||||||
|
snapAnimationSpec = tween(),
|
||||||
|
decayAnimationSpec = exponentialDecay()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dragState.currentValue == Anchors.Left || dragState.currentValue == Anchors.Right) {
|
||||||
|
onSwipe(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box( /* container for swipeable and it's background decoration */
|
||||||
|
modifier = modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.CenterStart
|
||||||
|
) {
|
||||||
|
|
||||||
|
decoration()
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.offset {
|
||||||
|
IntOffset(
|
||||||
|
x = dragState
|
||||||
|
.requireOffset()
|
||||||
|
.roundToInt(),
|
||||||
|
y = 0
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.background(colorResource(id = R.color.content_background)) // MUST BE AFTER .offset modifier (?!?!)
|
||||||
|
.anchoredDraggable(state = dragState, orientation = Orientation.Horizontal)
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue