fix: nwc ui
This commit is contained in:
@@ -415,5 +415,4 @@ class LNbitsWalletRepository(
|
|||||||
Timber.i("NWC [CONFIG SET] config updated ✓")
|
Timber.i("NWC [CONFIG SET] config updated ✓")
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,9 +109,8 @@ class PaymentCacheRepository(context: Context) {
|
|||||||
minCreatedAt = filter.minCreatedAt,
|
minCreatedAt = filter.minCreatedAt,
|
||||||
maxCreatedAt = filter.maxCreatedAt
|
maxCreatedAt = filter.maxCreatedAt
|
||||||
).map { it.toDomain() }.also {
|
).map { it.toDomain() }.also {
|
||||||
Timber.d("PAYMENTS [QUERY ] q=$searchQuery status=$statusPattern " +
|
Timber.d("%snull", "PAYMENTS [QUERY ] q=$searchQuery status=$statusPattern " +
|
||||||
"amt=${filter.minAmountSat}–${filter.maxAmountSat} " +
|
"amt=${filter.minAmountSat}–${filter.maxAmountSat} ")
|
||||||
"date=${filter.minCreatedAt}–${filter.maxCreatedAt} → ${it.size} rows")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -324,8 +324,8 @@ private fun BudgetCard(budget: BudgetData) {
|
|||||||
|
|
||||||
val totalSats = budget.budgetMsats / 1_000L
|
val totalSats = budget.budgetMsats / 1_000L
|
||||||
val usedSats = budget.usedBudgetMsats / 1_000L
|
val usedSats = budget.usedBudgetMsats / 1_000L
|
||||||
val progress = if (budget.budgetMsats > 0)
|
val progress = if (budget.budgetMsats > 0)
|
||||||
(budget.usedBudgetMsats.toFloat() / budget.budgetMsats.toFloat()).coerceIn(0f, 1f)
|
(1f - budget.usedBudgetMsats.toFloat() / budget.budgetMsats.toFloat()).coerceIn(0f, 1f)
|
||||||
else 0f
|
else 0f
|
||||||
val isOverBudget = budget.usedBudgetMsats > budget.budgetMsats
|
val isOverBudget = budget.usedBudgetMsats > budget.budgetMsats
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
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.KeyboardArrowRight
|
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.ArrowBack
|
|
||||||
import androidx.compose.material.icons.filled.Cable
|
import androidx.compose.material.icons.filled.Cable
|
||||||
|
import androidx.compose.material.icons.filled.CalendarToday
|
||||||
import androidx.compose.material.icons.filled.ContentCopy
|
import androidx.compose.material.icons.filled.ContentCopy
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
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
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
@@ -26,13 +28,15 @@ import androidx.compose.ui.text.font.FontFamily
|
|||||||
import androidx.compose.ui.text.style.TextOverflow
|
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.NwcBudget
|
|
||||||
import com.bitcointxoko.gudariwallet.api.NwcGetResponse
|
import com.bitcointxoko.gudariwallet.api.NwcGetResponse
|
||||||
import com.bitcointxoko.gudariwallet.ui.theme.semanticColors
|
import com.bitcointxoko.gudariwallet.ui.theme.semanticColors
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import java.time.format.FormatStyle
|
import java.time.format.FormatStyle
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
// ── Available permissions ─────────────────────────────────────────────────────
|
// ── Available permissions ─────────────────────────────────────────────────────
|
||||||
internal data class NwcPermission(
|
internal data class NwcPermission(
|
||||||
@@ -42,11 +46,11 @@ internal data class NwcPermission(
|
|||||||
)
|
)
|
||||||
internal val ALL_PERMISSIONS = listOf(
|
internal val ALL_PERMISSIONS = listOf(
|
||||||
NwcPermission("pay", "Send payments", default = true),
|
NwcPermission("pay", "Send payments", default = true),
|
||||||
NwcPermission("invoice", "Create invoices", default = true),
|
NwcPermission("invoice", "Create invoices", default = false),
|
||||||
NwcPermission("lookup", "Lookup invoice status", default = true),
|
NwcPermission("lookup", "Lookup invoice status", default = false),
|
||||||
NwcPermission("history", "Read transaction history", default = true),
|
NwcPermission("history", "Read transaction history", default = false),
|
||||||
NwcPermission("balance", "Read wallet balance", default = true),
|
NwcPermission("balance", "Read wallet balance", default = false),
|
||||||
NwcPermission("info", "Read account info", default = true)
|
NwcPermission("info", "Read account info", default = false)
|
||||||
)
|
)
|
||||||
|
|
||||||
// ── Screen ────────────────────────────────────────────────────────────────────
|
// ── Screen ────────────────────────────────────────────────────────────────────
|
||||||
@@ -72,7 +76,7 @@ fun NwcScreen(
|
|||||||
title = { Text("Wallet Connect") },
|
title = { Text("Wallet Connect") },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = onBack) {
|
||||||
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -88,7 +92,7 @@ fun NwcScreen(
|
|||||||
|
|
||||||
PullToRefreshBox(
|
PullToRefreshBox(
|
||||||
isRefreshing = isRefreshing,
|
isRefreshing = isRefreshing,
|
||||||
onRefresh = { viewModel.loadConnections() },
|
onRefresh = { viewModel.loadConnections(includeExpired = true) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(innerPadding)
|
.padding(innerPadding)
|
||||||
@@ -103,7 +107,7 @@ fun NwcScreen(
|
|||||||
is NwcUiState.Error -> {
|
is NwcUiState.Error -> {
|
||||||
NwcErrorContent(
|
NwcErrorContent(
|
||||||
message = state.message,
|
message = state.message,
|
||||||
onRetry = { viewModel.loadConnections() },
|
onRetry = { viewModel.loadConnections(includeExpired = true) },
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -201,7 +205,75 @@ private fun NwcAddConnectionSheet(
|
|||||||
var selectedPermissions by remember {
|
var selectedPermissions by remember {
|
||||||
mutableStateOf(ALL_PERMISSIONS.filter { it.default }.map { it.serverKey }.toSet())
|
mutableStateOf(ALL_PERMISSIONS.filter { it.default }.map { it.serverKey }.toSet())
|
||||||
}
|
}
|
||||||
var neverExpires by remember { mutableStateOf(true) }
|
var neverExpires by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
// Default expiry = now + 30 days, truncated to the minute
|
||||||
|
val defaultExpiry = remember {
|
||||||
|
Calendar.getInstance().apply {
|
||||||
|
add(Calendar.DAY_OF_YEAR, 30)
|
||||||
|
set(Calendar.SECOND, 0)
|
||||||
|
set(Calendar.MILLISECOND, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var expiryCalendar by remember { mutableStateOf(defaultExpiry) }
|
||||||
|
|
||||||
|
var showDatePicker by remember { mutableStateOf(false) }
|
||||||
|
var showTimePicker by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
// ── Date picker dialog ────────────────────────────────────────────────────
|
||||||
|
if (showDatePicker) {
|
||||||
|
val datePickerState = rememberDatePickerState(
|
||||||
|
initialSelectedDateMillis = expiryCalendar.timeInMillis
|
||||||
|
)
|
||||||
|
DatePickerDialog(
|
||||||
|
onDismissRequest = { showDatePicker = false },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
datePickerState.selectedDateMillis?.let { millis ->
|
||||||
|
expiryCalendar = (expiryCalendar.clone() as Calendar).apply {
|
||||||
|
val picked = Calendar.getInstance().apply { timeInMillis = millis }
|
||||||
|
set(Calendar.YEAR, picked.get(Calendar.YEAR))
|
||||||
|
set(Calendar.MONTH, picked.get(Calendar.MONTH))
|
||||||
|
set(Calendar.DAY_OF_MONTH, picked.get(Calendar.DAY_OF_MONTH))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
showDatePicker = false
|
||||||
|
}) { Text("OK") }
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showDatePicker = false }) { Text("Cancel") }
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
DatePicker(state = datePickerState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Time picker dialog ────────────────────────────────────────────────────
|
||||||
|
if (showTimePicker) {
|
||||||
|
val timePickerState = rememberTimePickerState(
|
||||||
|
initialHour = expiryCalendar.get(Calendar.HOUR_OF_DAY),
|
||||||
|
initialMinute = expiryCalendar.get(Calendar.MINUTE),
|
||||||
|
is24Hour = true
|
||||||
|
)
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { showTimePicker = false },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
expiryCalendar = (expiryCalendar.clone() as Calendar).apply {
|
||||||
|
set(Calendar.HOUR_OF_DAY, timePickerState.hour)
|
||||||
|
set(Calendar.MINUTE, timePickerState.minute)
|
||||||
|
set(Calendar.SECOND, 0)
|
||||||
|
set(Calendar.MILLISECOND, 0)
|
||||||
|
}
|
||||||
|
showTimePicker = false
|
||||||
|
}) { Text("OK") }
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = { showTimePicker = false }) { Text("Cancel") }
|
||||||
|
},
|
||||||
|
text = { TimePicker(state = timePickerState) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
@@ -215,7 +287,6 @@ private fun NwcAddConnectionSheet(
|
|||||||
.padding(bottom = 32.dp),
|
.padding(bottom = 32.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
) {
|
) {
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "New connection",
|
text = "New connection",
|
||||||
style = MaterialTheme.typography.titleMedium
|
style = MaterialTheme.typography.titleMedium
|
||||||
@@ -230,6 +301,72 @@ private fun NwcAddConnectionSheet(
|
|||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ── Expiry ────────────────────────────────────────────────────
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
// horizontalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Expires",
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
|
||||||
|
// Date chip — hidden when "Never" is checked
|
||||||
|
if (!neverExpires) {
|
||||||
|
val dateLabel = remember(expiryCalendar) {
|
||||||
|
SimpleDateFormat("dd MMM yyyy", Locale.getDefault())
|
||||||
|
.format(expiryCalendar.time)
|
||||||
|
}
|
||||||
|
SuggestionChip(
|
||||||
|
onClick = { showDatePicker = true },
|
||||||
|
label = { Text(dateLabel, style = MaterialTheme.typography.bodySmall) },
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.CalendarToday,
|
||||||
|
contentDescription = "Pick date",
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
|
||||||
|
// Time chip
|
||||||
|
val timeLabel = remember(expiryCalendar) {
|
||||||
|
String.format(
|
||||||
|
"%02d:%02d",
|
||||||
|
expiryCalendar.get(Calendar.HOUR_OF_DAY),
|
||||||
|
expiryCalendar.get(Calendar.MINUTE)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SuggestionChip(
|
||||||
|
onClick = { showTimePicker = true },
|
||||||
|
label = { Text(timeLabel, style = MaterialTheme.typography.bodySmall) },
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Schedule,
|
||||||
|
contentDescription = "Pick time",
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
|
||||||
|
Checkbox(
|
||||||
|
checked = neverExpires,
|
||||||
|
onCheckedChange = { neverExpires = it }
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Never",
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// ── Permissions ───────────────────────────────────────────────
|
// ── Permissions ───────────────────────────────────────────────
|
||||||
Text(
|
Text(
|
||||||
text = "Permissions",
|
text = "Permissions",
|
||||||
@@ -259,30 +396,15 @@ private fun NwcAddConnectionSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// ── Expiry ────────────────────────────────────────────────────
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Checkbox(
|
|
||||||
checked = neverExpires,
|
|
||||||
onCheckedChange = { neverExpires = it }
|
|
||||||
)
|
|
||||||
Spacer(Modifier.width(8.dp))
|
|
||||||
Text(
|
|
||||||
text = "Never expires",
|
|
||||||
style = MaterialTheme.typography.bodyMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Create button ─────────────────────────────────────────────
|
// ── Create button ─────────────────────────────────────────────
|
||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
|
val expiresAtSecs = if (neverExpires) 0L
|
||||||
|
else expiryCalendar.timeInMillis / 1000L
|
||||||
onCreate(
|
onCreate(
|
||||||
description.trim().ifBlank { "Unnamed" },
|
description.trim().ifBlank { "Unnamed" },
|
||||||
selectedPermissions.toList(),
|
selectedPermissions.toList(),
|
||||||
if (neverExpires) 0L else 0L // expiry picker can be added later
|
expiresAtSecs
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
enabled = selectedPermissions.isNotEmpty() && !isCreating,
|
enabled = selectedPermissions.isNotEmpty() && !isCreating,
|
||||||
@@ -290,9 +412,9 @@ private fun NwcAddConnectionSheet(
|
|||||||
) {
|
) {
|
||||||
if (isCreating) {
|
if (isCreating) {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
modifier = Modifier.size(18.dp),
|
modifier = Modifier.size(18.dp),
|
||||||
strokeWidth = 2.dp,
|
strokeWidth = 2.dp,
|
||||||
color = MaterialTheme.colorScheme.onPrimary
|
color = MaterialTheme.colorScheme.onPrimary
|
||||||
)
|
)
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
}
|
}
|
||||||
@@ -370,11 +492,11 @@ private fun NwcPairingUrlDialog(
|
|||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun NwcConnectionRow(
|
fun NwcConnectionRow(
|
||||||
connection : NwcGetResponse,
|
connection : NwcGetResponse,
|
||||||
isDeleting : Boolean,
|
isDeleting : Boolean,
|
||||||
onDeleteConfirm : () -> Unit,
|
onDeleteConfirm : () -> Unit,
|
||||||
onConnectionClick: () -> Unit,
|
onConnectionClick: () -> Unit,
|
||||||
modifier : Modifier = Modifier
|
modifier : Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val key = connection.data
|
val key = connection.data
|
||||||
val expired = key.expires_at > 0 && key.expires_at < Instant.now().epochSecond
|
val expired = key.expires_at > 0 && key.expires_at < Instant.now().epochSecond
|
||||||
@@ -382,24 +504,42 @@ fun NwcConnectionRow(
|
|||||||
|
|
||||||
// ── Swipe-to-delete state ────────────────────────────────────────────────
|
// ── Swipe-to-delete state ────────────────────────────────────────────────
|
||||||
val dismissState = rememberSwipeToDismissBoxState(
|
val dismissState = rememberSwipeToDismissBoxState(
|
||||||
confirmValueChange = { value ->
|
confirmValueChange = {
|
||||||
if (value == SwipeToDismissBoxValue.EndToStart && !isDeleting) {
|
// Always reject — we never want the row to stay dismissed.
|
||||||
onDeleteConfirm()
|
// The dialog is triggered via LaunchedEffect on targetValue below.
|
||||||
true
|
false
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
positionalThreshold = { totalDistance -> totalDistance * 0.4f }
|
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(
|
SwipeToDismissBox(
|
||||||
state = dismissState,
|
state = dismissState,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
enableDismissFromStartToEnd = false,
|
enableDismissFromStartToEnd = false,
|
||||||
enableDismissFromEndToStart = !isDeleting,
|
enableDismissFromEndToStart = !isDeleting,
|
||||||
backgroundContent = {
|
backgroundContent = {
|
||||||
// Red delete background revealed on swipe-left
|
|
||||||
val fraction = dismissState.progress
|
val fraction = dismissState.progress
|
||||||
val bgAlpha = (fraction / 0.4f).coerceIn(0f, 1f)
|
val bgAlpha = (fraction / 0.4f).coerceIn(0f, 1f)
|
||||||
Box(
|
Box(
|
||||||
@@ -417,7 +557,6 @@ fun NwcConnectionRow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
// ── Foreground row ───────────────────────────────────────────────────
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -426,9 +565,8 @@ fun NwcConnectionRow(
|
|||||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
// Leading icon – represents a wallet connection / plug
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Cable, // or Icons.Default.Link
|
imageVector = Icons.Default.Cable,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = if (expired)
|
tint = if (expired)
|
||||||
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f)
|
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f)
|
||||||
@@ -436,11 +574,8 @@ fun NwcConnectionRow(
|
|||||||
MaterialTheme.colorScheme.primary,
|
MaterialTheme.colorScheme.primary,
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(Modifier.width(12.dp))
|
Spacer(Modifier.width(12.dp))
|
||||||
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
// Connection name
|
|
||||||
Text(
|
Text(
|
||||||
text = key.description.ifBlank { "Unnamed connection" },
|
text = key.description.ifBlank { "Unnamed connection" },
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
@@ -451,18 +586,13 @@ fun NwcConnectionRow(
|
|||||||
else
|
else
|
||||||
MaterialTheme.colorScheme.onSurface
|
MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(Modifier.height(2.dp))
|
Spacer(Modifier.height(2.dp))
|
||||||
|
|
||||||
// Last-used timestamp
|
|
||||||
Text(
|
Text(
|
||||||
text = if (key.last_used <= 0L) "Never used"
|
text = if (key.last_used <= 0L) "Never used"
|
||||||
else "Last used ${formatEpoch(key.last_used)}",
|
else "Last used ${formatEpoch(key.last_used)}",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|
||||||
// Expired badge – mirrors the pending/failed chip in PaymentRow
|
|
||||||
if (expired) {
|
if (expired) {
|
||||||
Spacer(Modifier.height(4.dp))
|
Spacer(Modifier.height(4.dp))
|
||||||
Surface(
|
Surface(
|
||||||
@@ -478,8 +608,6 @@ fun NwcConnectionRow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subtle chevron – signals the row is tappable
|
|
||||||
Spacer(Modifier.width(8.dp))
|
Spacer(Modifier.width(8.dp))
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.AutoMirrored.Default.KeyboardArrowRight,
|
imageVector = Icons.AutoMirrored.Default.KeyboardArrowRight,
|
||||||
@@ -491,34 +619,6 @@ fun NwcConnectionRow(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Budget row ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun NwcBudgetRow(budget: NwcBudget, modifier: Modifier = Modifier) {
|
|
||||||
val totalSats = budget.budget_msats / 1_000L
|
|
||||||
val usedSats = budget.used_budget_msats / 1_000L
|
|
||||||
val progress = if (totalSats > 0) (usedSats.toFloat() / totalSats.toFloat()).coerceIn(0f, 1f) else 0f
|
|
||||||
val windowHours = budget.refresh_window / 3600
|
|
||||||
|
|
||||||
Column(modifier = modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
|
||||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
|
||||||
Text("Budget (${windowHours}h window)", style = MaterialTheme.typography.labelMedium)
|
|
||||||
Text(
|
|
||||||
"$usedSats / $totalSats sats",
|
|
||||||
style = MaterialTheme.typography.labelMedium,
|
|
||||||
color = if (progress >= 1f) MaterialTheme.colorScheme.error
|
|
||||||
else MaterialTheme.colorScheme.onSurfaceVariant
|
|
||||||
)
|
|
||||||
}
|
|
||||||
LinearProgressIndicator(
|
|
||||||
progress = { progress },
|
|
||||||
modifier = Modifier.fillMaxWidth().height(6.dp),
|
|
||||||
color = if (progress >= 1f) MaterialTheme.colorScheme.error
|
|
||||||
else MaterialTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Empty / Error states ──────────────────────────────────────────────────────
|
// ── Empty / Error states ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
Reference in New Issue
Block a user