refactor: extract connection row from nwc
This commit is contained in:
@@ -0,0 +1,147 @@
|
|||||||
|
package com.bitcointxoko.gudariwallet.ui.nwc
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||||
|
import androidx.compose.material.icons.filled.Cable
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.bitcointxoko.gudariwallet.api.NwcGetResponse
|
||||||
|
import com.bitcointxoko.gudariwallet.ui.theme.semanticColors
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun NwcConnectionRow(
|
||||||
|
connection : NwcGetResponse,
|
||||||
|
isDeleting : Boolean,
|
||||||
|
onDeleteConfirm : () -> Unit,
|
||||||
|
onConnectionClick: () -> Unit,
|
||||||
|
modifier : Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val key = connection.data
|
||||||
|
val expired = key.expires_at > 0 && key.expires_at < Instant.now().epochSecond
|
||||||
|
val semantic = semanticColors()
|
||||||
|
|
||||||
|
// ── Swipe-to-delete state ────────────────────────────────────────────────
|
||||||
|
val dismissState = rememberSwipeToDismissBoxState(
|
||||||
|
confirmValueChange = {
|
||||||
|
// Always reject — we never want the row to stay dismissed.
|
||||||
|
// The dialog is triggered via LaunchedEffect on targetValue below.
|
||||||
|
false
|
||||||
|
},
|
||||||
|
positionalThreshold = { totalDistance -> totalDistance * 0.4f }
|
||||||
|
)
|
||||||
|
|
||||||
|
// targetValue changes to EndToStart as soon as the drag crosses the
|
||||||
|
// threshold, even though confirmValueChange returns false and currentValue
|
||||||
|
// stays at Settled. Use a flag so we only fire once per swipe gesture.
|
||||||
|
var dialogTriggered by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
LaunchedEffect(dismissState.targetValue) {
|
||||||
|
if (dismissState.targetValue == SwipeToDismissBoxValue.EndToStart
|
||||||
|
&& !isDeleting
|
||||||
|
&& !dialogTriggered
|
||||||
|
) {
|
||||||
|
dialogTriggered = true
|
||||||
|
onDeleteConfirm()
|
||||||
|
// Snap the row back to its resting position.
|
||||||
|
dismissState.reset()
|
||||||
|
}
|
||||||
|
// Reset the flag once the swipe returns to Settled so the
|
||||||
|
// gesture can be triggered again if the user cancels and re-swipes.
|
||||||
|
if (dismissState.targetValue == SwipeToDismissBoxValue.Settled) {
|
||||||
|
dialogTriggered = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SwipeToDismissBox(
|
||||||
|
state = dismissState,
|
||||||
|
modifier = modifier,
|
||||||
|
enableDismissFromStartToEnd = false,
|
||||||
|
enableDismissFromEndToStart = !isDeleting,
|
||||||
|
backgroundContent = {
|
||||||
|
val fraction = dismissState.progress
|
||||||
|
val bgAlpha = (fraction / 0.4f).coerceIn(0f, 1f)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colorScheme.error.copy(alpha = bgAlpha))
|
||||||
|
.padding(end = 24.dp),
|
||||||
|
contentAlignment = Alignment.CenterEnd
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Delete,
|
||||||
|
contentDescription = "Delete connection",
|
||||||
|
tint = MaterialTheme.colorScheme.onError.copy(alpha = bgAlpha)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
|
.clickable(onClick = onConnectionClick)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Cable,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = if (expired)
|
||||||
|
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f)
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(12.dp))
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = key.description.ifBlank { "Unnamed connection" },
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
color = if (expired)
|
||||||
|
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f)
|
||||||
|
else
|
||||||
|
MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(2.dp))
|
||||||
|
Text(
|
||||||
|
text = if (key.last_used <= 0L) "Never used"
|
||||||
|
else "Last used ${formatEpoch(key.last_used)}",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
if (expired) {
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
Surface(
|
||||||
|
shape = MaterialTheme.shapes.extraSmall,
|
||||||
|
color = semantic.errorContainer,
|
||||||
|
contentColor = semantic.onErrorContainer
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Expired",
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Default.KeyboardArrowRight,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,8 +5,6 @@ import android.content.ActivityNotFoundException
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
@@ -15,12 +13,9 @@ import androidx.compose.foundation.text.KeyboardOptions
|
|||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.Cable
|
|
||||||
import androidx.compose.material.icons.filled.CalendarToday
|
import androidx.compose.material.icons.filled.CalendarToday
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.Delete
|
|
||||||
import androidx.compose.material.icons.filled.Schedule
|
import androidx.compose.material.icons.filled.Schedule
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
@@ -29,15 +24,11 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.bitcointxoko.gudariwallet.api.NwcGetResponse
|
|
||||||
import com.bitcointxoko.gudariwallet.api.NwcNewBudget
|
import com.bitcointxoko.gudariwallet.api.NwcNewBudget
|
||||||
import com.bitcointxoko.gudariwallet.ui.receive.QrDisplayCard
|
import com.bitcointxoko.gudariwallet.ui.receive.QrDisplayCard
|
||||||
import com.bitcointxoko.gudariwallet.ui.theme.semanticColors
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.time.Instant
|
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
@@ -610,134 +601,3 @@ private fun NwcPairingUrlDialog(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun NwcConnectionRow(
|
|
||||||
connection : NwcGetResponse,
|
|
||||||
isDeleting : Boolean,
|
|
||||||
onDeleteConfirm : () -> Unit,
|
|
||||||
onConnectionClick: () -> Unit,
|
|
||||||
modifier : Modifier = Modifier
|
|
||||||
) {
|
|
||||||
val key = connection.data
|
|
||||||
val expired = key.expires_at > 0 && key.expires_at < Instant.now().epochSecond
|
|
||||||
val semantic = semanticColors()
|
|
||||||
|
|
||||||
// ── Swipe-to-delete state ────────────────────────────────────────────────
|
|
||||||
val dismissState = rememberSwipeToDismissBoxState(
|
|
||||||
confirmValueChange = {
|
|
||||||
// Always reject — we never want the row to stay dismissed.
|
|
||||||
// The dialog is triggered via LaunchedEffect on targetValue below.
|
|
||||||
false
|
|
||||||
},
|
|
||||||
positionalThreshold = { totalDistance -> totalDistance * 0.4f }
|
|
||||||
)
|
|
||||||
|
|
||||||
// targetValue changes to EndToStart as soon as the drag crosses the
|
|
||||||
// threshold, even though confirmValueChange returns false and currentValue
|
|
||||||
// stays at Settled. Use a flag so we only fire once per swipe gesture.
|
|
||||||
var dialogTriggered by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
LaunchedEffect(dismissState.targetValue) {
|
|
||||||
if (dismissState.targetValue == SwipeToDismissBoxValue.EndToStart
|
|
||||||
&& !isDeleting
|
|
||||||
&& !dialogTriggered
|
|
||||||
) {
|
|
||||||
dialogTriggered = true
|
|
||||||
onDeleteConfirm()
|
|
||||||
// Snap the row back to its resting position.
|
|
||||||
dismissState.reset()
|
|
||||||
}
|
|
||||||
// Reset the flag once the swipe returns to Settled so the
|
|
||||||
// gesture can be triggered again if the user cancels and re-swipes.
|
|
||||||
if (dismissState.targetValue == SwipeToDismissBoxValue.Settled) {
|
|
||||||
dialogTriggered = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SwipeToDismissBox(
|
|
||||||
state = dismissState,
|
|
||||||
modifier = modifier,
|
|
||||||
enableDismissFromStartToEnd = false,
|
|
||||||
enableDismissFromEndToStart = !isDeleting,
|
|
||||||
backgroundContent = {
|
|
||||||
val fraction = dismissState.progress
|
|
||||||
val bgAlpha = (fraction / 0.4f).coerceIn(0f, 1f)
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(MaterialTheme.colorScheme.error.copy(alpha = bgAlpha))
|
|
||||||
.padding(end = 24.dp),
|
|
||||||
contentAlignment = Alignment.CenterEnd
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Delete,
|
|
||||||
contentDescription = "Delete connection",
|
|
||||||
tint = MaterialTheme.colorScheme.onError.copy(alpha = bgAlpha)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
|
||||||
.clickable(onClick = onConnectionClick)
|
|
||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Cable,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = if (expired)
|
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f)
|
|
||||||
else
|
|
||||||
MaterialTheme.colorScheme.primary,
|
|
||||||
modifier = Modifier.size(20.dp)
|
|
||||||
)
|
|
||||||
Spacer(Modifier.width(12.dp))
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
|
||||||
Text(
|
|
||||||
text = key.description.ifBlank { "Unnamed connection" },
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
color = if (expired)
|
|
||||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f)
|
|
||||||
else
|
|
||||||
MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
|
||||||
Spacer(Modifier.height(2.dp))
|
|
||||||
Text(
|
|
||||||
text = if (key.last_used <= 0L) "Never used"
|
|
||||||
else "Last used ${formatEpoch(key.last_used)}",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
if (expired) {
|
|
||||||
Spacer(Modifier.height(4.dp))
|
|
||||||
Surface(
|
|
||||||
shape = MaterialTheme.shapes.extraSmall,
|
|
||||||
color = semantic.errorContainer,
|
|
||||||
contentColor = semantic.onErrorContainer
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = "Expired",
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
|
||||||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(Modifier.width(8.dp))
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.AutoMirrored.Default.KeyboardArrowRight,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),
|
|
||||||
modifier = Modifier.size(16.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user