refactor: extract add connection sheet from nwc
This commit is contained in:
@@ -0,0 +1,375 @@
|
||||
package com.bitcointxoko.gudariwallet.ui.nwc
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.CalendarToday
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Schedule
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.bitcointxoko.gudariwallet.api.NwcNewBudget
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
|
||||
// ── Add connection bottom sheet ──────────────────────────────────────────────
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
internal fun NwcAddConnectionSheet(
|
||||
isCreating : Boolean,
|
||||
onCreate : (
|
||||
description : String,
|
||||
permissions : List<String>,
|
||||
expiresAt : Long,
|
||||
budgets : List<NwcNewBudget>
|
||||
) -> Unit,
|
||||
onDismiss : () -> Unit
|
||||
) {
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
|
||||
var description by remember { mutableStateOf("") }
|
||||
var selectedPermissions by remember {
|
||||
mutableStateOf(ALL_PERMISSIONS.filter { it.default }.map { it.serverKey }.toSet())
|
||||
}
|
||||
var neverExpires by remember { mutableStateOf(false) }
|
||||
|
||||
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) }
|
||||
|
||||
// ── Budget list state ────────────────────────────────────────────────────
|
||||
var nextBudgetId by remember { mutableIntStateOf(0) }
|
||||
var budgets by remember { mutableStateOf(emptyList<BudgetDraft>()) }
|
||||
|
||||
// ── 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(
|
||||
onDismissRequest = onDismiss,
|
||||
sheetState = sheetState
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 24.dp)
|
||||
.padding(bottom = 32.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "New connection",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
// ── Description ──────────────────────────────────────────────────
|
||||
OutlinedTextField(
|
||||
value = description,
|
||||
onValueChange = { description = it },
|
||||
label = { Text("Label (e.g. Amethyst, Bitrefill)") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
// ── Expiry ───────────────────────────────────────────────────────
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(text = "Expires", style = MaterialTheme.typography.bodyMedium)
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
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))
|
||||
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)
|
||||
}
|
||||
|
||||
// ── Budgets ──────────────────────────────────────────────────────
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Budgets",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(Modifier.weight(1f))
|
||||
TextButton(
|
||||
onClick = {
|
||||
budgets = budgets + BudgetDraft(id = nextBudgetId)
|
||||
nextBudgetId++
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = "Add budget",
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Add budget")
|
||||
}
|
||||
}
|
||||
|
||||
budgets.forEach { draft ->
|
||||
BudgetRow(
|
||||
draft = draft,
|
||||
onChange = { updated ->
|
||||
budgets = budgets.map { if (it.id == draft.id) updated else it }
|
||||
},
|
||||
onRemove = {
|
||||
budgets = budgets.filter { it.id != draft.id }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// ── Permissions ──────────────────────────────────────────────────
|
||||
Text(
|
||||
text = "Permissions",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
ALL_PERMISSIONS.forEach { perm ->
|
||||
val checked = perm.serverKey in selectedPermissions
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(
|
||||
checked = checked,
|
||||
onCheckedChange = { on ->
|
||||
selectedPermissions = if (on)
|
||||
selectedPermissions + perm.serverKey
|
||||
else
|
||||
selectedPermissions - perm.serverKey
|
||||
}
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(text = perm.displayLabel, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Create button ────────────────────────────────────────────────
|
||||
Button(
|
||||
onClick = {
|
||||
val expiresAtSecs = if (neverExpires) 0L
|
||||
else expiryCalendar.timeInMillis / 1000L
|
||||
val validBudgets = budgets.mapNotNull { it.toNwcNewBudget() }
|
||||
onCreate(
|
||||
description.trim().ifBlank { "Unnamed" },
|
||||
selectedPermissions.toList(),
|
||||
expiresAtSecs,
|
||||
validBudgets
|
||||
)
|
||||
},
|
||||
enabled = selectedPermissions.isNotEmpty() && !isCreating,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
if (isCreating) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(18.dp),
|
||||
strokeWidth = 2.dp,
|
||||
color = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
}
|
||||
Text(if (isCreating) "Creating…" else "Create connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Budget row ───────────────────────────────────────────────────────────────
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun BudgetRow(
|
||||
draft : BudgetDraft,
|
||||
onChange : (BudgetDraft) -> Unit,
|
||||
onRemove : () -> Unit
|
||||
) {
|
||||
var dropdownExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
// Shared height for both fields so they are always flush
|
||||
val fieldHeight = 64.dp
|
||||
|
||||
Surface(
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
tonalElevation = 2.dp,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
// ── Amount field ─────────────────────────────────────────────────
|
||||
OutlinedTextField(
|
||||
value = draft.amountSats,
|
||||
onValueChange = { onChange(draft.copy(amountSats = it.filter(Char::isDigit))) },
|
||||
label = { Text("Sats") },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(fieldHeight)
|
||||
)
|
||||
|
||||
// ── Refresh-window dropdown ──────────────────────────────────────
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = dropdownExpanded,
|
||||
onExpandedChange = { dropdownExpanded = it },
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(fieldHeight)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = draft.refreshWindow.label,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text("Resets") },
|
||||
trailingIcon = {
|
||||
ExposedDropdownMenuDefaults.TrailingIcon(expanded = dropdownExpanded)
|
||||
},
|
||||
modifier = Modifier
|
||||
.menuAnchor()
|
||||
.fillMaxWidth()
|
||||
.height(fieldHeight)
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = dropdownExpanded,
|
||||
onDismissRequest = { dropdownExpanded = false }
|
||||
) {
|
||||
BudgetRefreshWindow.entries.forEach { window ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(window.label) },
|
||||
onClick = {
|
||||
onChange(draft.copy(refreshWindow = window))
|
||||
dropdownExpanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Remove button ────────────────────────────────────────────────
|
||||
IconButton(
|
||||
onClick = onRemove,
|
||||
modifier = Modifier.size(fieldHeight)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "Remove budget",
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,27 +3,16 @@ package com.bitcointxoko.gudariwallet.ui.nwc
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.CalendarToday
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Schedule
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.bitcointxoko.gudariwallet.api.NwcNewBudget
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
|
||||
// ── Screen ────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -161,356 +150,4 @@ fun NwcScreen(
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Add connection bottom sheet ───────────────────────────────────────────────
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun NwcAddConnectionSheet(
|
||||
isCreating : Boolean,
|
||||
onCreate : (
|
||||
description : String,
|
||||
permissions : List<String>,
|
||||
expiresAt : Long,
|
||||
budgets : List<NwcNewBudget> // ← NEW
|
||||
) -> Unit,
|
||||
onDismiss : () -> Unit
|
||||
) {
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
|
||||
var description by remember { mutableStateOf("") }
|
||||
var selectedPermissions by remember {
|
||||
mutableStateOf(ALL_PERMISSIONS.filter { it.default }.map { it.serverKey }.toSet())
|
||||
}
|
||||
var neverExpires by remember { mutableStateOf(false) }
|
||||
|
||||
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) }
|
||||
|
||||
// ── Budget list state ──────────────────────────────────────────────────────
|
||||
var nextBudgetId by remember { mutableIntStateOf(0) }
|
||||
var budgets by remember { mutableStateOf(emptyList<BudgetDraft>()) }
|
||||
|
||||
// ── 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(
|
||||
onDismissRequest = onDismiss,
|
||||
sheetState = sheetState
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 24.dp)
|
||||
.padding(bottom = 32.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "New connection",
|
||||
style = MaterialTheme.typography.titleMedium
|
||||
)
|
||||
|
||||
// ── Description ───────────────────────────────────────────────────
|
||||
OutlinedTextField(
|
||||
value = description,
|
||||
onValueChange = { description = it },
|
||||
label = { Text("Label (e.g. Amethyst, Bitrefill)") },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
// ── Expiry ────────────────────────────────────────────────────────
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(text = "Expires", style = MaterialTheme.typography.bodyMedium)
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
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))
|
||||
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)
|
||||
}
|
||||
|
||||
// ── Budgets ───────────────────────────────────────────────────────
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "Budgets",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Spacer(Modifier.weight(1f))
|
||||
TextButton(
|
||||
onClick = {
|
||||
budgets = budgets + BudgetDraft(id = nextBudgetId)
|
||||
nextBudgetId++
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = "Add budget",
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(Modifier.width(4.dp))
|
||||
Text("Add budget")
|
||||
}
|
||||
}
|
||||
|
||||
budgets.forEach { draft ->
|
||||
BudgetRow(
|
||||
draft = draft,
|
||||
onChange = { updated ->
|
||||
budgets = budgets.map { if (it.id == draft.id) updated else it }
|
||||
},
|
||||
onRemove = {
|
||||
budgets = budgets.filter { it.id != draft.id }
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// ── Permissions ───────────────────────────────────────────────────
|
||||
Text(
|
||||
text = "Permissions",
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
ALL_PERMISSIONS.forEach { perm ->
|
||||
val checked = perm.serverKey in selectedPermissions
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(
|
||||
checked = checked,
|
||||
onCheckedChange = { on ->
|
||||
selectedPermissions = if (on)
|
||||
selectedPermissions + perm.serverKey
|
||||
else
|
||||
selectedPermissions - perm.serverKey
|
||||
}
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(text = perm.displayLabel, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Create button ─────────────────────────────────────────────────
|
||||
Button(
|
||||
onClick = {
|
||||
val expiresAtSecs = if (neverExpires) 0L
|
||||
else expiryCalendar.timeInMillis / 1000L
|
||||
val validBudgets = budgets.mapNotNull { it.toNwcNewBudget() }
|
||||
onCreate(
|
||||
description.trim().ifBlank { "Unnamed" },
|
||||
selectedPermissions.toList(),
|
||||
expiresAtSecs,
|
||||
validBudgets
|
||||
)
|
||||
},
|
||||
enabled = selectedPermissions.isNotEmpty() && !isCreating,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
if (isCreating) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(18.dp),
|
||||
strokeWidth = 2.dp,
|
||||
color = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
}
|
||||
Text(if (isCreating) "Creating…" else "Create connection")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun BudgetRow(
|
||||
draft : BudgetDraft,
|
||||
onChange : (BudgetDraft) -> Unit,
|
||||
onRemove : () -> Unit
|
||||
) {
|
||||
var dropdownExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
// Shared height for both fields so they are always flush
|
||||
val fieldHeight = 64.dp
|
||||
|
||||
Surface(
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
tonalElevation = 2.dp,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
// ── Amount field ──────────────────────────────────────────────────
|
||||
OutlinedTextField(
|
||||
value = draft.amountSats,
|
||||
onValueChange = { onChange(draft.copy(amountSats = it.filter(Char::isDigit))) },
|
||||
label = { Text("Sats") },
|
||||
singleLine = true,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(fieldHeight)
|
||||
)
|
||||
|
||||
// ── Refresh-window dropdown ───────────────────────────────────────
|
||||
ExposedDropdownMenuBox(
|
||||
expanded = dropdownExpanded,
|
||||
onExpandedChange = { dropdownExpanded = it },
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.height(fieldHeight)
|
||||
) {
|
||||
OutlinedTextField(
|
||||
value = draft.refreshWindow.label,
|
||||
onValueChange = {},
|
||||
readOnly = true,
|
||||
label = { Text("Resets") },
|
||||
trailingIcon = {
|
||||
ExposedDropdownMenuDefaults.TrailingIcon(expanded = dropdownExpanded)
|
||||
},
|
||||
modifier = Modifier
|
||||
.menuAnchor()
|
||||
.fillMaxWidth()
|
||||
.height(fieldHeight)
|
||||
)
|
||||
ExposedDropdownMenu(
|
||||
expanded = dropdownExpanded,
|
||||
onDismissRequest = { dropdownExpanded = false }
|
||||
) {
|
||||
BudgetRefreshWindow.entries.forEach { window ->
|
||||
DropdownMenuItem(
|
||||
text = { Text(window.label) },
|
||||
onClick = {
|
||||
onChange(draft.copy(refreshWindow = window))
|
||||
dropdownExpanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Remove button ─────────────────────────────────────────────────
|
||||
IconButton(
|
||||
onClick = onRemove,
|
||||
modifier = Modifier.size(fieldHeight) // matches field height
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = "Remove budget",
|
||||
tint = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user