Update bonded device item, handle confirm before delete bonded device item
This commit is contained in:
+161
-60
@@ -1,73 +1,185 @@
|
||||
package com.digitoolsolutions.app.torquevaultkmp.screens.bonded.view
|
||||
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
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.gestures.animateTo
|
||||
import androidx.compose.foundation.gestures.snapTo
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Bluetooth
|
||||
import androidx.compose.material.icons.filled.DeleteSweep
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.material3.SwipeToDismissBox
|
||||
import androidx.compose.material3.SwipeToDismissBoxDefaults
|
||||
import androidx.compose.material3.SwipeToDismissBoxState
|
||||
import androidx.compose.material3.SwipeToDismissBoxValue
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.rememberSwipeToDismissBoxState
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.lerp
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.digitoolsolutions.app.torquevaultkmp.components.CircularIcon
|
||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.db.Device
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
enum class DragValue {
|
||||
Settled,
|
||||
Revealed
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun BondedDeviceItem(device: Device, onDelete: (Device) -> Unit, modifier: Modifier = Modifier) {
|
||||
val swipeToDismissBoxState = rememberSwipeToDismissBoxState(
|
||||
SwipeToDismissBoxValue.Settled,
|
||||
SwipeToDismissBoxDefaults.positionalThreshold
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
val density = LocalDensity.current
|
||||
val actionSize = 80.dp
|
||||
val actionSizePx = with(density) { actionSize.toPx() }
|
||||
|
||||
val state = remember {
|
||||
AnchoredDraggableState(
|
||||
initialValue = DragValue.Settled
|
||||
)
|
||||
}
|
||||
|
||||
if (swipeToDismissBoxState.currentValue == SwipeToDismissBoxValue.EndToStart) {
|
||||
LaunchedEffect(Unit) {
|
||||
SideEffect {
|
||||
state.updateAnchors(
|
||||
DraggableAnchors {
|
||||
DragValue.Settled at 0f
|
||||
DragValue.Revealed at -actionSizePx
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (showDeleteDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = {
|
||||
showDeleteDialog = false
|
||||
scope.launch { state.animateTo(DragValue.Settled) }
|
||||
},
|
||||
title = { Text("Delete Device") },
|
||||
text = { Text("Are you sure want to delete bonded device ${device.name}?") },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onDelete(device)
|
||||
swipeToDismissBoxState.snapTo(SwipeToDismissBoxValue.Settled)
|
||||
}
|
||||
}
|
||||
|
||||
SwipeToDismissBox(
|
||||
state = swipeToDismissBoxState,
|
||||
modifier = modifier.fillMaxSize().padding(vertical = 4.dp),
|
||||
enableDismissFromStartToEnd = false,
|
||||
backgroundContent = {
|
||||
when (swipeToDismissBoxState.dismissDirection) {
|
||||
SwipeToDismissBoxValue.StartToEnd -> {
|
||||
SwipeToDismissBackgroundItem(label = "Delete", contentAlignment = Alignment.CenterStart, state = swipeToDismissBoxState)
|
||||
}
|
||||
SwipeToDismissBoxValue.EndToStart -> {
|
||||
SwipeToDismissBackgroundItem(label = "Delete", state = swipeToDismissBoxState)
|
||||
}
|
||||
SwipeToDismissBoxValue.Settled -> {}
|
||||
}
|
||||
showDeleteDialog = false
|
||||
scope.launch { state.snapTo(DragValue.Settled) }
|
||||
}
|
||||
) {
|
||||
Text("Delete", color = MaterialTheme.colorScheme.error)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = {
|
||||
showDeleteDialog = false
|
||||
scope.launch { state.animateTo(DragValue.Settled) }
|
||||
}) {
|
||||
Text("Cancel")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(IntrinsicSize.Min)
|
||||
.padding(vertical = 4.dp)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.border(
|
||||
BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
|
||||
RoundedCornerShape(8.dp)
|
||||
)
|
||||
) {
|
||||
// Action Layer (Background)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterEnd)
|
||||
.fillMaxHeight()
|
||||
.width(actionSize)
|
||||
.background(MaterialTheme.colorScheme.errorContainer)
|
||||
.clickable { showDeleteDialog = true },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.DeleteSweep,
|
||||
contentDescription = "Delete",
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
Text(
|
||||
text = "Delete",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Content Layer (Foreground)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.offset {
|
||||
IntOffset(
|
||||
x = state.offset.roundToInt(),
|
||||
y = 0
|
||||
)
|
||||
}
|
||||
.anchoredDraggable(state, Orientation.Horizontal)
|
||||
) {
|
||||
OutlinedCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.combinedClickable(
|
||||
onClick = { /* Action on click if needed */ },
|
||||
onLongClick = { showMenu = true }
|
||||
),
|
||||
shape = RectangleShape,
|
||||
colors = CardDefaults.outlinedCardColors(
|
||||
containerColor = if (showMenu) MaterialTheme.colorScheme.surfaceVariant else MaterialTheme.colorScheme.surface
|
||||
),
|
||||
border = CardDefaults.outlinedCardBorder(enabled = true),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = if (showMenu) 8.dp else 4.dp)
|
||||
) {
|
||||
Row (
|
||||
modifier = Modifier.padding(16.dp),
|
||||
@@ -88,37 +200,26 @@ fun BondedDeviceItem(device: Device, onDelete: (Device) -> Unit, modifier: Modif
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun SwipeToDismissBackgroundItem(
|
||||
label: String,
|
||||
contentAlignment: Alignment = Alignment.CenterEnd,
|
||||
icon: @Composable () -> Unit = {
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Delete") },
|
||||
onClick = {
|
||||
showMenu = false
|
||||
showDeleteDialog = true
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.DeleteSweep,
|
||||
contentDescription = "Delete",
|
||||
tint = Color.White
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
},
|
||||
state: SwipeToDismissBoxState
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(lerp(Color.LightGray, Color.Red, state.progress))
|
||||
.padding(end = 20.dp),
|
||||
contentAlignment = contentAlignment
|
||||
) {
|
||||
Row {
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
icon()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-1
@@ -50,7 +50,7 @@ class ScannerViewModel(
|
||||
) : ViewModel() {
|
||||
companion object {
|
||||
private val SCAN_TIMEOUT = 1.seconds
|
||||
private val CONNECTION_TIMEOUT = 15.seconds
|
||||
private val CONNECTION_TIMEOUT = 5.seconds
|
||||
}
|
||||
private val _devices = MutableStateFlow<List<Advertisement>>(emptyList())
|
||||
val devices: StateFlow<List<Advertisement>> = _devices.asStateFlow()
|
||||
@@ -272,6 +272,7 @@ class ScannerViewModel(
|
||||
sendWorkOrdersToDevice(peripheral,orders)
|
||||
} catch (e: Exception) {
|
||||
Napier.e("Failed to load work orders", e)
|
||||
logRepository.addLog(title = "HTTP", content = "${e.message}".substringBefore("[url="))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -312,6 +313,7 @@ class ScannerViewModel(
|
||||
} catch (e: Exception) {
|
||||
Napier.e(">>>>> Failed to handle message: $msg", e)
|
||||
sendCommand(peripheral, Helper.CMD_NOT_AVAILABLE_WO)
|
||||
logRepository.addLog(title = "HTTP", content = "${e.message}".substringBefore("[url="))
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
@@ -332,6 +334,7 @@ class ScannerViewModel(
|
||||
scannerRepository.sendMeasurementResult(woId, dto)
|
||||
} catch (e: Exception) {
|
||||
Napier.e("An error occurred while sending the measurement", e, tag = "ScannerViewModel")
|
||||
logRepository.addLog(title = "HTTP", content = "${e.message}".substringBefore("[url="))
|
||||
} finally {
|
||||
// Currently, always send UPLOADED_WO regardless of success or failure
|
||||
sendCommand(peripheral, Helper.CMD_UPLOADED_WO)
|
||||
@@ -342,6 +345,7 @@ class ScannerViewModel(
|
||||
scannerRepository.sendMeasurementResult(woId, dto, true)
|
||||
} catch (e: Exception) {
|
||||
Napier.e("An error occurred while sending the re-measurement", e, tag = "ScannerViewModel")
|
||||
logRepository.addLog(title = "HTTP", content = "${e.message}".substringBefore("[url="))
|
||||
} finally {
|
||||
// Currently, always send UPLOADED_WO regardless of success or failure
|
||||
sendCommand(peripheral, Helper.CMD_UPLOADED_WO)
|
||||
@@ -352,6 +356,7 @@ class ScannerViewModel(
|
||||
scannerRepository.sendCancelWorkOrder(woId, dto)
|
||||
} catch (e: Exception) {
|
||||
Napier.e("An error occurred while cancel work order", e, tag = "ScannerViewModel")
|
||||
logRepository.addLog(title = "HTTP", content = "${e.message}".substringBefore("[url="))
|
||||
} finally {
|
||||
// Currently, always send UPLOADED_WO regardless of success or failure
|
||||
sendCommand(peripheral, Helper.CMD_UPLOADED_WO)
|
||||
|
||||
Reference in New Issue
Block a user