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
|
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.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.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
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.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Bluetooth
|
import androidx.compose.material.icons.filled.Bluetooth
|
||||||
import androidx.compose.material.icons.filled.DeleteSweep
|
import androidx.compose.material.icons.filled.DeleteSweep
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.CardDefaults
|
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.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedCard
|
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.Text
|
||||||
import androidx.compose.material3.rememberSwipeToDismissBoxState
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.lerp
|
import androidx.compose.ui.graphics.RectangleShape
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.components.CircularIcon
|
import com.digitoolsolutions.app.torquevaultkmp.components.CircularIcon
|
||||||
import com.digitoolsolutions.app.torquevaultkmp.data.storage.db.Device
|
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
|
@Composable
|
||||||
fun BondedDeviceItem(device: Device, onDelete: (Device) -> Unit, modifier: Modifier = Modifier) {
|
fun BondedDeviceItem(device: Device, onDelete: (Device) -> Unit, modifier: Modifier = Modifier) {
|
||||||
val swipeToDismissBoxState = rememberSwipeToDismissBoxState(
|
var showMenu by remember { mutableStateOf(false) }
|
||||||
SwipeToDismissBoxValue.Settled,
|
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||||
SwipeToDismissBoxDefaults.positionalThreshold
|
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) {
|
SideEffect {
|
||||||
LaunchedEffect(Unit) {
|
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)
|
onDelete(device)
|
||||||
swipeToDismissBoxState.snapTo(SwipeToDismissBoxValue.Settled)
|
showDeleteDialog = false
|
||||||
}
|
scope.launch { state.snapTo(DragValue.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 -> {}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
) {
|
||||||
|
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(
|
OutlinedCard(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
|
.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 (
|
Row (
|
||||||
modifier = Modifier.padding(16.dp),
|
modifier = Modifier.padding(16.dp),
|
||||||
@@ -88,37 +200,26 @@ fun BondedDeviceItem(device: Device, onDelete: (Device) -> Unit, modifier: Modif
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
DropdownMenu(
|
||||||
internal fun SwipeToDismissBackgroundItem(
|
expanded = showMenu,
|
||||||
label: String,
|
onDismissRequest = { showMenu = false }
|
||||||
contentAlignment: Alignment = Alignment.CenterEnd,
|
) {
|
||||||
icon: @Composable () -> Unit = {
|
DropdownMenuItem(
|
||||||
|
text = { Text("Delete") },
|
||||||
|
onClick = {
|
||||||
|
showMenu = false
|
||||||
|
showDeleteDialog = true
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.DeleteSweep,
|
imageVector = Icons.Default.DeleteSweep,
|
||||||
contentDescription = "Delete",
|
contentDescription = null,
|
||||||
tint = Color.White
|
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() {
|
) : ViewModel() {
|
||||||
companion object {
|
companion object {
|
||||||
private val SCAN_TIMEOUT = 1.seconds
|
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())
|
private val _devices = MutableStateFlow<List<Advertisement>>(emptyList())
|
||||||
val devices: StateFlow<List<Advertisement>> = _devices.asStateFlow()
|
val devices: StateFlow<List<Advertisement>> = _devices.asStateFlow()
|
||||||
@@ -272,6 +272,7 @@ class ScannerViewModel(
|
|||||||
sendWorkOrdersToDevice(peripheral,orders)
|
sendWorkOrdersToDevice(peripheral,orders)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Napier.e("Failed to load work orders", e)
|
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) {
|
} catch (e: Exception) {
|
||||||
Napier.e(">>>>> Failed to handle message: $msg", e)
|
Napier.e(">>>>> Failed to handle message: $msg", e)
|
||||||
sendCommand(peripheral, Helper.CMD_NOT_AVAILABLE_WO)
|
sendCommand(peripheral, Helper.CMD_NOT_AVAILABLE_WO)
|
||||||
|
logRepository.addLog(title = "HTTP", content = "${e.message}".substringBefore("[url="))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
@@ -332,6 +334,7 @@ class ScannerViewModel(
|
|||||||
scannerRepository.sendMeasurementResult(woId, dto)
|
scannerRepository.sendMeasurementResult(woId, dto)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Napier.e("An error occurred while sending the measurement", e, tag = "ScannerViewModel")
|
Napier.e("An error occurred while sending the measurement", e, tag = "ScannerViewModel")
|
||||||
|
logRepository.addLog(title = "HTTP", content = "${e.message}".substringBefore("[url="))
|
||||||
} finally {
|
} finally {
|
||||||
// Currently, always send UPLOADED_WO regardless of success or failure
|
// Currently, always send UPLOADED_WO regardless of success or failure
|
||||||
sendCommand(peripheral, Helper.CMD_UPLOADED_WO)
|
sendCommand(peripheral, Helper.CMD_UPLOADED_WO)
|
||||||
@@ -342,6 +345,7 @@ class ScannerViewModel(
|
|||||||
scannerRepository.sendMeasurementResult(woId, dto, true)
|
scannerRepository.sendMeasurementResult(woId, dto, true)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Napier.e("An error occurred while sending the re-measurement", e, tag = "ScannerViewModel")
|
Napier.e("An error occurred while sending the re-measurement", e, tag = "ScannerViewModel")
|
||||||
|
logRepository.addLog(title = "HTTP", content = "${e.message}".substringBefore("[url="))
|
||||||
} finally {
|
} finally {
|
||||||
// Currently, always send UPLOADED_WO regardless of success or failure
|
// Currently, always send UPLOADED_WO regardless of success or failure
|
||||||
sendCommand(peripheral, Helper.CMD_UPLOADED_WO)
|
sendCommand(peripheral, Helper.CMD_UPLOADED_WO)
|
||||||
@@ -352,6 +356,7 @@ class ScannerViewModel(
|
|||||||
scannerRepository.sendCancelWorkOrder(woId, dto)
|
scannerRepository.sendCancelWorkOrder(woId, dto)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Napier.e("An error occurred while cancel work order", e, tag = "ScannerViewModel")
|
Napier.e("An error occurred while cancel work order", e, tag = "ScannerViewModel")
|
||||||
|
logRepository.addLog(title = "HTTP", content = "${e.message}".substringBefore("[url="))
|
||||||
} finally {
|
} finally {
|
||||||
// Currently, always send UPLOADED_WO regardless of success or failure
|
// Currently, always send UPLOADED_WO regardless of success or failure
|
||||||
sendCommand(peripheral, Helper.CMD_UPLOADED_WO)
|
sendCommand(peripheral, Helper.CMD_UPLOADED_WO)
|
||||||
|
|||||||
Reference in New Issue
Block a user