feat: localisation with lyricist
This commit is contained in:
@@ -94,7 +94,23 @@ val EnHomeStrings = AppStrings(
|
|||||||
// ── ReceiveScreen — success ───────────────────────────────────────────────
|
// ── ReceiveScreen — success ───────────────────────────────────────────────
|
||||||
paymentReceived = "Payment Received!",
|
paymentReceived = "Payment Received!",
|
||||||
|
|
||||||
// ── SendScreen — general ──────────────────────────────────────────────────
|
// ── AmountDisplay ─────────────────────────────────────────────────────────
|
||||||
|
amountDisplaySats = { amount -> "$amount ${if (amount == 1L) "sat" else "sats"}" },
|
||||||
|
|
||||||
|
// ── BrightnessToggleButton & QrDisplayCard ────────────────────────────────
|
||||||
|
brightnessReduce = "Reduce brightness",
|
||||||
|
brightnessBoost = "Boost brightness",
|
||||||
|
qrShare = "Share",
|
||||||
|
qrCopy = "Copy",
|
||||||
|
qrNfcStop = "Stop NFC sharing",
|
||||||
|
qrNfcStart = "Share via NFC",
|
||||||
|
|
||||||
|
// ── ReceiveViewModel ──────────────────────────────────────────────────────
|
||||||
|
receiveVmFailedToCreateInvoice = "Failed to create invoice",
|
||||||
|
receiveVmWithdrawRejected = "Withdraw service rejected the invoice",
|
||||||
|
receiveVmWithdrawFailed = "Withdraw failed",
|
||||||
|
|
||||||
|
// ── SendScreen — general ──────────────────────────────────────────────────
|
||||||
send = "Send",
|
send = "Send",
|
||||||
paste = "Paste",
|
paste = "Paste",
|
||||||
continueButton = "Continue",
|
continueButton = "Continue",
|
||||||
@@ -159,4 +175,124 @@ val EnHomeStrings = AppStrings(
|
|||||||
routeHintHideDetails = "Hide details ▴",
|
routeHintHideDetails = "Hide details ▴",
|
||||||
routeHintHopsHeader = "Private route hint hops",
|
routeHintHopsHeader = "Private route hint hops",
|
||||||
routeHintHopFees = { ppm, baseLabel -> "$ppm ppm · $baseLabel base" },
|
routeHintHopFees = { ppm, baseLabel -> "$ppm ppm · $baseLabel base" },
|
||||||
|
|
||||||
|
// ── HistoryScreen ─────────────────────────────────────────────────────────
|
||||||
|
historyTitle = "History",
|
||||||
|
historyClose = "Close",
|
||||||
|
historySearchPayments = "Search payments",
|
||||||
|
historyFilterPayments = "Filter payments",
|
||||||
|
historyRetry = "Retry",
|
||||||
|
historyNoPayments = "No payments yet.",
|
||||||
|
historySearchPlaceholder = "Search transactions",
|
||||||
|
historySearchHelp = "Search help",
|
||||||
|
historySearchPaste = "Paste",
|
||||||
|
historySearchClear = "Clear search",
|
||||||
|
historySearchChip = { tail -> "search: …$tail" },
|
||||||
|
historyClearSearch = "Clear search",
|
||||||
|
filterDirectionOutgoing = "Outgoing",
|
||||||
|
filterDirectionIncoming = "Incoming",
|
||||||
|
filterClearDirection = "Clear direction filter",
|
||||||
|
filterStatusCompleted = "Completed",
|
||||||
|
filterStatusPending = "Pending",
|
||||||
|
filterStatusFailed = "Failed",
|
||||||
|
filterRemoveStatus = "Remove status filter",
|
||||||
|
filterRemoveType = "Remove type filter",
|
||||||
|
filterAmountRange = { min, max -> "$min–$max sats" },
|
||||||
|
filterAmountMin = { min -> "≥ $min sats" },
|
||||||
|
filterAmountMax = { max -> "≤ $max sats" },
|
||||||
|
filterClearAmount = "Clear amount filter",
|
||||||
|
filterDateThisWeek = "This week",
|
||||||
|
filterDateThisMonth = "This month",
|
||||||
|
filterDateThisYear = "This year",
|
||||||
|
filterDateRange = { from, to -> "$from–$to" },
|
||||||
|
filterDateFrom = { from -> "From $from" },
|
||||||
|
filterDateUntil = { until -> "Until $until" },
|
||||||
|
filterClearDate = "Clear date filter",
|
||||||
|
|
||||||
|
// ── FilterBottomSheet ─────────────────────────────────────────────────────
|
||||||
|
filterSheetTitle = "Filter payments",
|
||||||
|
filterSectionDirection = "Direction",
|
||||||
|
filterDirectionAll = "All",
|
||||||
|
filterSectionStatus = "Status",
|
||||||
|
filterSectionType = "Type",
|
||||||
|
filterSectionAmount = "Amount (sats)",
|
||||||
|
filterAmountMin_label = "Min",
|
||||||
|
filterAmountMax_label = "Max",
|
||||||
|
filterAmountSuffix = "sats",
|
||||||
|
filterSectionDate = "Date",
|
||||||
|
filterDateFrom_label = "From",
|
||||||
|
filterDateTo_label = "To",
|
||||||
|
filterDialogOk = "OK",
|
||||||
|
filterDialogCancel = "Cancel",
|
||||||
|
|
||||||
|
// ── SearchInfoSheet ───────────────────────────────────────────────────────
|
||||||
|
searchInfoTitle = "What can I search for?",
|
||||||
|
searchInfoSubtitle = "Type any part of the following to find a payment:",
|
||||||
|
searchInfoMemoTitle = "Memo",
|
||||||
|
searchInfoMemoDesc = "The message or description attached to a payment. " +
|
||||||
|
"For example, \"coffee\" or \"rent March\".",
|
||||||
|
searchInfoInvoiceTitle = "Invoice",
|
||||||
|
searchInfoInvoiceDesc = "The payment request you scanned or pasted to send a payment. " +
|
||||||
|
"It usually starts with \"lnbc…\". " +
|
||||||
|
"You can paste just the first or last few characters.",
|
||||||
|
searchInfoHashTitle = "Hash",
|
||||||
|
searchInfoHashDesc = "A unique code that identifies this payment on the " +
|
||||||
|
"Lightning Network. Useful if someone asks you to " +
|
||||||
|
"confirm a specific transaction.",
|
||||||
|
searchInfoPreimageTitle = "Preimage",
|
||||||
|
searchInfoPreimageDesc = "A secret code that proves a payment was received. " +
|
||||||
|
"Only available after a payment completes successfully.",
|
||||||
|
searchInfoNodeAliasTitle = "Node alias",
|
||||||
|
searchInfoNodeAliasDesc = "The name of the Lightning node you sent a payment to. " +
|
||||||
|
"For example, \"ACINQ\" or \"Wallet of Satoshi\". " +
|
||||||
|
"Aliases are resolved automatically for outgoing payments.",
|
||||||
|
searchInfoNodeIdTitle = "Node ID (pubkey)",
|
||||||
|
searchInfoNodeIdDesc = "The public key of the destination node. " +
|
||||||
|
"A 66-character hex string — you can paste just the first few characters.",
|
||||||
|
searchInfoFooter = "You can paste a full value or search with just a few characters — " +
|
||||||
|
"partial matches work too.",
|
||||||
|
|
||||||
|
// ── PaymentDetailScreen ───────────────────────────────────────────────────
|
||||||
|
detailOutgoingTitle = "Outgoing payment",
|
||||||
|
detailIncomingTitle = "Incoming payment",
|
||||||
|
detailBack = "Back",
|
||||||
|
detailCouldNotLoad = "Could not load full details",
|
||||||
|
detailFeeLabel = { feeSat, fiatPart, ppmPart ->
|
||||||
|
"Fee: $feeSat sats$fiatPart$ppmPart"
|
||||||
|
},
|
||||||
|
detailSectionDetails = "Details",
|
||||||
|
detailRowDate = "Date",
|
||||||
|
detailRowMemo = "Memo",
|
||||||
|
detailRowMemoEmpty = "—",
|
||||||
|
detailRowType = "Type",
|
||||||
|
detailRowComment = "Comment",
|
||||||
|
detailSectionSuccessAction = "Success Action",
|
||||||
|
detailSuccessMessage = "Message",
|
||||||
|
detailSuccessUrl = "URL",
|
||||||
|
detailSuccessDescription = "Description",
|
||||||
|
detailSectionTechnical = "Technical",
|
||||||
|
detailRowPaymentHash = "Hash",
|
||||||
|
detailRowPreimage = "Preimage",
|
||||||
|
detailRowDestination = "Destination",
|
||||||
|
detailCopyNodeId = "Copy node ID",
|
||||||
|
detailRowBolt11 = "BOLT11",
|
||||||
|
|
||||||
|
// ── PaymentRow ────────────────────────────────────────────────────────────
|
||||||
|
paymentRowSentCD = "Sent",
|
||||||
|
paymentRowReceivedCD = "Received",
|
||||||
|
paymentRowNoMemo = "No memo",
|
||||||
|
paymentRowPending = "Pending",
|
||||||
|
|
||||||
|
// ── OnboardingScreen ──────────────────────────────────────────────────────
|
||||||
|
onboardingTitle = "Connect to LNbits",
|
||||||
|
onboardingSubtitle = "Enter your LNbits server URL and API keys.",
|
||||||
|
onboardingServerUrl = "Server URL",
|
||||||
|
onboardingServerUrlHint = "https://bitcointxoko.org",
|
||||||
|
onboardingInvoiceKey = "Invoice Key (read-only)",
|
||||||
|
onboardingAdminKey = "Admin Key (for sending)",
|
||||||
|
onboardingHideAdminKey = "Hide",
|
||||||
|
onboardingShowAdminKey = "Show",
|
||||||
|
onboardingFieldsRequired = "All fields are required.",
|
||||||
|
onboardingConnect = "Connect",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,23 @@ val EsHomeStrings = AppStrings(
|
|||||||
// ── ReceiveScreen — success ───────────────────────────────────────────────
|
// ── ReceiveScreen — success ───────────────────────────────────────────────
|
||||||
paymentReceived = "¡Pago recibido!",
|
paymentReceived = "¡Pago recibido!",
|
||||||
|
|
||||||
// ── SendScreen — general ──────────────────────────────────────────────────
|
// ── AmountDisplay ─────────────────────────────────────────────────────────
|
||||||
|
amountDisplaySats = { amount -> "$amount ${if (amount == 1L) "sat" else "sats"}" },
|
||||||
|
|
||||||
|
// ── BrightnessToggleButton & QrDisplayCard ────────────────────────────────
|
||||||
|
brightnessReduce = "Reducir brillo",
|
||||||
|
brightnessBoost = "Aumentar brillo",
|
||||||
|
qrShare = "Compartir",
|
||||||
|
qrCopy = "Copiar",
|
||||||
|
qrNfcStop = "Detener NFC",
|
||||||
|
qrNfcStart = "Compartir por NFC",
|
||||||
|
|
||||||
|
// ── ReceiveViewModel ──────────────────────────────────────────────────────
|
||||||
|
receiveVmFailedToCreateInvoice = "No se pudo crear la factura",
|
||||||
|
receiveVmWithdrawRejected = "El servicio de retiro rechazó la factura",
|
||||||
|
receiveVmWithdrawFailed = "Error al retirar",
|
||||||
|
|
||||||
|
// ── SendScreen — general ──────────────────────────────────────────────────
|
||||||
send = "Enviar",
|
send = "Enviar",
|
||||||
paste = "Pegar",
|
paste = "Pegar",
|
||||||
continueButton = "Continuar",
|
continueButton = "Continuar",
|
||||||
@@ -159,4 +175,123 @@ val EsHomeStrings = AppStrings(
|
|||||||
routeHintHideDetails = "Ocultar detalles ▴",
|
routeHintHideDetails = "Ocultar detalles ▴",
|
||||||
routeHintHopsHeader = "Saltos de ruta privada",
|
routeHintHopsHeader = "Saltos de ruta privada",
|
||||||
routeHintHopFees = { ppm, baseLabel -> "$ppm ppm · $baseLabel base" },
|
routeHintHopFees = { ppm, baseLabel -> "$ppm ppm · $baseLabel base" },
|
||||||
|
|
||||||
|
// ── HistoryScreen ─────────────────────────────────────────────────────────
|
||||||
|
historyTitle = "Historial",
|
||||||
|
historyClose = "Cerrar",
|
||||||
|
historySearchPayments = "Buscar pagos",
|
||||||
|
historyFilterPayments = "Filtrar pagos",
|
||||||
|
historyRetry = "Reintentar",
|
||||||
|
historyNoPayments = "Aún no hay pagos.",
|
||||||
|
historySearchPlaceholder = "Buscar transacciones",
|
||||||
|
historySearchHelp = "Ayuda de búsqueda",
|
||||||
|
historySearchPaste = "Pegar",
|
||||||
|
historySearchClear = "Limpiar búsqueda",
|
||||||
|
historySearchChip = { tail -> "búsqueda: …$tail" },
|
||||||
|
historyClearSearch = "Limpiar búsqueda",
|
||||||
|
filterDirectionOutgoing = "Salientes",
|
||||||
|
filterDirectionIncoming = "Entrantes",
|
||||||
|
filterClearDirection = "Quitar filtro de dirección",
|
||||||
|
filterStatusCompleted = "Completado",
|
||||||
|
filterStatusPending = "Pendiente",
|
||||||
|
filterStatusFailed = "Fallido",
|
||||||
|
filterRemoveStatus = "Quitar filtro de estado",
|
||||||
|
filterRemoveType = "Quitar filtro de tipo",
|
||||||
|
filterAmountRange = { min, max -> "$min–$max sats" },
|
||||||
|
filterAmountMin = { min -> "≥ $min sats" },
|
||||||
|
filterAmountMax = { max -> "≤ $max sats" },
|
||||||
|
filterClearAmount = "Quitar filtro de cantidad",
|
||||||
|
filterDateThisWeek = "Esta semana",
|
||||||
|
filterDateThisMonth = "Este mes",
|
||||||
|
filterDateThisYear = "Este año",
|
||||||
|
filterDateRange = { from, to -> "$from–$to" },
|
||||||
|
filterDateFrom = { from -> "Desde $from" },
|
||||||
|
filterDateUntil = { until -> "Hasta $until" },
|
||||||
|
filterClearDate = "Quitar filtro de fecha",
|
||||||
|
|
||||||
|
// ── FilterBottomSheet ─────────────────────────────────────────────────────
|
||||||
|
filterSheetTitle = "Filtrar pagos",
|
||||||
|
filterSectionDirection = "Dirección",
|
||||||
|
filterDirectionAll = "Todos",
|
||||||
|
filterSectionStatus = "Estado",
|
||||||
|
filterSectionType = "Tipo",
|
||||||
|
filterSectionAmount = "Cantidad (sats)",
|
||||||
|
filterAmountMin_label = "Mín.",
|
||||||
|
filterAmountMax_label = "Máx.",
|
||||||
|
filterAmountSuffix = "sats",
|
||||||
|
filterSectionDate = "Fecha",
|
||||||
|
filterDateFrom_label = "Desde",
|
||||||
|
filterDateTo_label = "Hasta",
|
||||||
|
filterDialogOk = "Aceptar",
|
||||||
|
filterDialogCancel = "Cancelar",
|
||||||
|
|
||||||
|
// ── SearchInfoSheet ───────────────────────────────────────────────────────
|
||||||
|
searchInfoTitle = "¿Qué puedo buscar?",
|
||||||
|
searchInfoSubtitle = "Escribe cualquier parte de lo siguiente para encontrar un pago:",
|
||||||
|
searchInfoMemoTitle = "Nota",
|
||||||
|
searchInfoMemoDesc = "El mensaje o descripción adjunto a un pago. " +
|
||||||
|
"Por ejemplo, \"café\" o \"alquiler marzo\".",
|
||||||
|
searchInfoInvoiceTitle = "Factura",
|
||||||
|
searchInfoInvoiceDesc = "La solicitud de pago que escaneaste o pegaste para enviar un pago. " +
|
||||||
|
"Normalmente empieza por \"lnbc…\". " +
|
||||||
|
"Puedes pegar solo los primeros o últimos caracteres.",
|
||||||
|
searchInfoHashTitle = "Hash",
|
||||||
|
searchInfoHashDesc = "Un código único que identifica este pago en la " +
|
||||||
|
"red Lightning. Útil si alguien te pide que " +
|
||||||
|
"confirmes una transacción específica.",
|
||||||
|
searchInfoPreimageTitle = "Preimagen",
|
||||||
|
searchInfoPreimageDesc = "Un código secreto que demuestra que se recibió un pago. " +
|
||||||
|
"Solo disponible después de que un pago se complete correctamente.",
|
||||||
|
searchInfoNodeAliasTitle = "Alias del nodo",
|
||||||
|
searchInfoNodeAliasDesc = "El nombre del nodo Lightning al que enviaste un pago. " +
|
||||||
|
"Por ejemplo, \"ACINQ\" o \"Wallet of Satoshi\". " +
|
||||||
|
"Los alias se resuelven automáticamente para pagos salientes.",
|
||||||
|
searchInfoNodeIdTitle = "ID del nodo (clave pública)",
|
||||||
|
searchInfoNodeIdDesc = "La clave pública del nodo de destino. " +
|
||||||
|
"Una cadena hexadecimal de 66 caracteres — puedes pegar solo los primeros.",
|
||||||
|
searchInfoFooter = "Puedes pegar un valor completo o buscar con solo unos pocos caracteres — " +
|
||||||
|
"las coincidencias parciales también funcionan.",
|
||||||
|
|
||||||
|
// ── PaymentDetailScreen ───────────────────────────────────────────────────
|
||||||
|
detailOutgoingTitle = "Pago saliente",
|
||||||
|
detailIncomingTitle = "Pago entrante",
|
||||||
|
detailBack = "Atrás",
|
||||||
|
detailCouldNotLoad = "No se pudieron cargar los detalles completos",
|
||||||
|
detailFeeLabel = { feeSat, fiatPart, ppmPart ->
|
||||||
|
"Comisión: $feeSat sats$fiatPart$ppmPart"
|
||||||
|
},
|
||||||
|
detailSectionDetails = "Detalles",
|
||||||
|
detailRowDate = "Fecha",
|
||||||
|
detailRowMemo = "Nota",
|
||||||
|
detailRowMemoEmpty = "—",
|
||||||
|
detailRowType = "Tipo",
|
||||||
|
detailRowComment = "Comentario",
|
||||||
|
detailSectionSuccessAction = "Acción de éxito",
|
||||||
|
detailSuccessMessage = "Mensaje",
|
||||||
|
detailSuccessUrl = "URL",
|
||||||
|
detailSuccessDescription = "Descripción",
|
||||||
|
detailSectionTechnical = "Técnico",
|
||||||
|
detailRowPaymentHash = "Hash",
|
||||||
|
detailRowPreimage = "Preimagen",
|
||||||
|
detailRowDestination = "Destino",
|
||||||
|
detailCopyNodeId = "Copiar ID del nodo",
|
||||||
|
detailRowBolt11 = "BOLT11",
|
||||||
|
|
||||||
|
// ── PaymentRow ────────────────────────────────────────────────────────────
|
||||||
|
paymentRowSentCD = "Enviado",
|
||||||
|
paymentRowReceivedCD = "Recibido",
|
||||||
|
paymentRowNoMemo = "Sin nota",
|
||||||
|
paymentRowPending = "Pendiente",
|
||||||
|
|
||||||
|
// ── OnboardingScreen ──────────────────────────────────────────────────────
|
||||||
|
onboardingTitle = "Conectar a LNbits",
|
||||||
|
onboardingSubtitle = "Introduce la URL de tu servidor LNbits y las claves API.",
|
||||||
|
onboardingServerUrl = "URL del servidor",
|
||||||
|
onboardingServerUrlHint = "https://bitcointxoko.org",
|
||||||
|
onboardingInvoiceKey = "Clave de factura (solo lectura)",
|
||||||
|
onboardingAdminKey = "Clave de administrador (para enviar)",
|
||||||
|
onboardingHideAdminKey = "Ocultar",
|
||||||
|
onboardingShowAdminKey = "Mostrar",
|
||||||
|
onboardingFieldsRequired = "Todos los campos son obligatorios.",
|
||||||
|
onboardingConnect = "Conectar",
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -103,6 +103,22 @@ data class AppStrings(
|
|||||||
// ── ReceiveScreen — success ───────────────────────────────────────────────
|
// ── ReceiveScreen — success ───────────────────────────────────────────────
|
||||||
val paymentReceived : String,
|
val paymentReceived : String,
|
||||||
|
|
||||||
|
// ── AmountDisplay ─────────────────────────────────────────────────────────
|
||||||
|
val amountDisplaySats : (amount: Long) -> String,
|
||||||
|
|
||||||
|
// ── BrightnessToggleButton & QrDisplayCard ────────────────────────────────
|
||||||
|
val brightnessReduce : String,
|
||||||
|
val brightnessBoost : String,
|
||||||
|
val qrShare : String,
|
||||||
|
val qrCopy : String,
|
||||||
|
val qrNfcStop : String,
|
||||||
|
val qrNfcStart : String,
|
||||||
|
|
||||||
|
// ── ReceiveViewModel ──────────────────────────────────────────────────────
|
||||||
|
val receiveVmFailedToCreateInvoice : String,
|
||||||
|
val receiveVmWithdrawRejected : String,
|
||||||
|
val receiveVmWithdrawFailed : String,
|
||||||
|
|
||||||
// ── SendScreen — general ──────────────────────────────────────────────────
|
// ── SendScreen — general ──────────────────────────────────────────────────
|
||||||
val send : String,
|
val send : String,
|
||||||
val paste : String,
|
val paste : String,
|
||||||
@@ -168,4 +184,111 @@ data class AppStrings(
|
|||||||
val routeHintHideDetails : String,
|
val routeHintHideDetails : String,
|
||||||
val routeHintHopsHeader : String,
|
val routeHintHopsHeader : String,
|
||||||
val routeHintHopFees : (ppm: Long, baseLabel: String) -> String,
|
val routeHintHopFees : (ppm: Long, baseLabel: String) -> String,
|
||||||
|
|
||||||
|
// ── HistoryScreen ─────────────────────────────────────────────────────────
|
||||||
|
val historyTitle : String,
|
||||||
|
val historyClose : String,
|
||||||
|
val historySearchPayments : String,
|
||||||
|
val historyFilterPayments : String,
|
||||||
|
val historyRetry : String,
|
||||||
|
val historyNoPayments : String,
|
||||||
|
val historySearchPlaceholder : String,
|
||||||
|
val historySearchHelp : String,
|
||||||
|
val historySearchPaste : String,
|
||||||
|
val historySearchClear : String,
|
||||||
|
val historySearchChip : (tail: String) -> String,
|
||||||
|
val historyClearSearch : String,
|
||||||
|
val filterDirectionOutgoing : String,
|
||||||
|
val filterDirectionIncoming : String,
|
||||||
|
val filterClearDirection : String,
|
||||||
|
val filterStatusCompleted : String,
|
||||||
|
val filterStatusPending : String,
|
||||||
|
val filterStatusFailed : String,
|
||||||
|
val filterRemoveStatus : String,
|
||||||
|
val filterRemoveType : String,
|
||||||
|
val filterAmountRange : (min: Long, max: Long) -> String,
|
||||||
|
val filterAmountMin : (min: Long) -> String,
|
||||||
|
val filterAmountMax : (max: Long) -> String,
|
||||||
|
val filterClearAmount : String,
|
||||||
|
val filterDateThisWeek : String,
|
||||||
|
val filterDateThisMonth : String,
|
||||||
|
val filterDateThisYear : String,
|
||||||
|
val filterDateRange : (from: String, to: String) -> String,
|
||||||
|
val filterDateFrom : (from: String) -> String,
|
||||||
|
val filterDateUntil : (until: String) -> String,
|
||||||
|
val filterClearDate : String,
|
||||||
|
|
||||||
|
// ── FilterBottomSheet ─────────────────────────────────────────────────────
|
||||||
|
val filterSheetTitle : String,
|
||||||
|
val filterSectionDirection : String,
|
||||||
|
val filterDirectionAll : String,
|
||||||
|
val filterSectionStatus : String,
|
||||||
|
val filterSectionType : String,
|
||||||
|
val filterSectionAmount : String,
|
||||||
|
val filterAmountMin_label : String,
|
||||||
|
val filterAmountMax_label : String,
|
||||||
|
val filterAmountSuffix : String,
|
||||||
|
val filterSectionDate : String,
|
||||||
|
val filterDateFrom_label : String,
|
||||||
|
val filterDateTo_label : String,
|
||||||
|
val filterDialogOk : String,
|
||||||
|
val filterDialogCancel : String,
|
||||||
|
|
||||||
|
// ── SearchInfoSheet ───────────────────────────────────────────────────────
|
||||||
|
val searchInfoTitle : String,
|
||||||
|
val searchInfoSubtitle : String,
|
||||||
|
val searchInfoMemoTitle : String,
|
||||||
|
val searchInfoMemoDesc : String,
|
||||||
|
val searchInfoInvoiceTitle : String,
|
||||||
|
val searchInfoInvoiceDesc : String,
|
||||||
|
val searchInfoHashTitle : String,
|
||||||
|
val searchInfoHashDesc : String,
|
||||||
|
val searchInfoPreimageTitle : String,
|
||||||
|
val searchInfoPreimageDesc : String,
|
||||||
|
val searchInfoNodeAliasTitle : String,
|
||||||
|
val searchInfoNodeAliasDesc : String,
|
||||||
|
val searchInfoNodeIdTitle : String,
|
||||||
|
val searchInfoNodeIdDesc : String,
|
||||||
|
val searchInfoFooter : String,
|
||||||
|
|
||||||
|
// ── PaymentDetailScreen ───────────────────────────────────────────────────
|
||||||
|
val detailOutgoingTitle : String,
|
||||||
|
val detailIncomingTitle : String,
|
||||||
|
val detailBack : String,
|
||||||
|
val detailCouldNotLoad : String,
|
||||||
|
val detailFeeLabel : (feeSat: Long, fiatPart: String, ppmPart: String) -> String,
|
||||||
|
val detailSectionDetails : String,
|
||||||
|
val detailRowDate : String,
|
||||||
|
val detailRowMemo : String,
|
||||||
|
val detailRowMemoEmpty : String,
|
||||||
|
val detailRowType : String,
|
||||||
|
val detailRowComment : String,
|
||||||
|
val detailSectionSuccessAction: String,
|
||||||
|
val detailSuccessMessage : String,
|
||||||
|
val detailSuccessUrl : String,
|
||||||
|
val detailSuccessDescription : String,
|
||||||
|
val detailSectionTechnical : String,
|
||||||
|
val detailRowPaymentHash : String,
|
||||||
|
val detailRowPreimage : String,
|
||||||
|
val detailRowDestination : String,
|
||||||
|
val detailCopyNodeId : String,
|
||||||
|
val detailRowBolt11 : String,
|
||||||
|
|
||||||
|
// ── PaymentRow ────────────────────────────────────────────────────────────
|
||||||
|
val paymentRowSentCD : String,
|
||||||
|
val paymentRowReceivedCD : String,
|
||||||
|
val paymentRowNoMemo : String,
|
||||||
|
val paymentRowPending : String,
|
||||||
|
|
||||||
|
// ── OnboardingScreen ──────────────────────────────────────────────────────
|
||||||
|
val onboardingTitle : String,
|
||||||
|
val onboardingSubtitle : String,
|
||||||
|
val onboardingServerUrl : String,
|
||||||
|
val onboardingServerUrlHint : String,
|
||||||
|
val onboardingInvoiceKey : String,
|
||||||
|
val onboardingAdminKey : String,
|
||||||
|
val onboardingHideAdminKey : String,
|
||||||
|
val onboardingShowAdminKey : String,
|
||||||
|
val onboardingFieldsRequired : String,
|
||||||
|
val onboardingConnect : String,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,24 +1,45 @@
|
|||||||
package com.bitcointxoko.gudariwallet.ui
|
package com.bitcointxoko.gudariwallet.ui
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Visibility
|
import androidx.compose.material.icons.filled.Visibility
|
||||||
import androidx.compose.material.icons.filled.VisibilityOff
|
import androidx.compose.material.icons.filled.VisibilityOff
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
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.text.input.*
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.bitcointxoko.gudariwallet.LocalAppStrings
|
||||||
import com.bitcointxoko.gudariwallet.security.SecretStore
|
import com.bitcointxoko.gudariwallet.security.SecretStore
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun OnboardingScreen(
|
fun OnboardingScreen(
|
||||||
secretStore: SecretStore,
|
secretStore: SecretStore,
|
||||||
onComplete: () -> Unit
|
onComplete : () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val strings = LocalAppStrings.current
|
||||||
|
|
||||||
var baseUrl by remember { mutableStateOf("https://bitcointxoko.org") }
|
var baseUrl by remember { mutableStateOf("https://bitcointxoko.org") }
|
||||||
var invoiceKey by remember { mutableStateOf("") }
|
var invoiceKey by remember { mutableStateOf("") }
|
||||||
var adminKey by remember { mutableStateOf("") }
|
var adminKey by remember { mutableStateOf("") }
|
||||||
@@ -26,60 +47,63 @@ fun OnboardingScreen(
|
|||||||
var error by remember { mutableStateOf<String?>(null) }
|
var error by remember { mutableStateOf<String?>(null) }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(32.dp),
|
.padding(32.dp),
|
||||||
verticalArrangement = Arrangement.Center,
|
verticalArrangement = Arrangement.Center,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Text("Connect to LNbits", style = MaterialTheme.typography.headlineMedium)
|
Text(
|
||||||
|
text = strings.onboardingTitle, // ← "Connect to LNbits"
|
||||||
|
style = MaterialTheme.typography.headlineMedium
|
||||||
|
)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
"Enter your LNbits server URL and API keys.",
|
text = strings.onboardingSubtitle, // ← "Enter your LNbits server URL and API keys."
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(32.dp))
|
Spacer(Modifier.height(32.dp))
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = baseUrl,
|
value = baseUrl,
|
||||||
onValueChange = { baseUrl = it },
|
onValueChange = { baseUrl = it },
|
||||||
label = { Text("Server URL") },
|
label = { Text(strings.onboardingServerUrl) }, // ← "Server URL"
|
||||||
placeholder = { Text("https://bitcointxoko.org") },
|
placeholder = { Text(strings.onboardingServerUrlHint) }, // ← "https://bitcointxoko.org"
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri)
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri)
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = invoiceKey,
|
value = invoiceKey,
|
||||||
onValueChange = { invoiceKey = it },
|
onValueChange = { invoiceKey = it },
|
||||||
label = { Text("Invoice Key (read-only)") },
|
label = { Text(strings.onboardingInvoiceKey) }, // ← "Invoice Key (read-only)"
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||||
visualTransformation = PasswordVisualTransformation()
|
visualTransformation = PasswordVisualTransformation()
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = adminKey,
|
value = adminKey,
|
||||||
onValueChange = { adminKey = it },
|
onValueChange = { adminKey = it },
|
||||||
label = { Text("Admin Key (for sending)") },
|
label = { Text(strings.onboardingAdminKey) }, // ← "Admin Key (for sending)"
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
|
||||||
visualTransformation = if (adminVisible) VisualTransformation.None
|
visualTransformation = if (adminVisible) VisualTransformation.None
|
||||||
else PasswordVisualTransformation(),
|
else PasswordVisualTransformation(),
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
IconButton(onClick = { adminVisible = !adminVisible }) {
|
IconButton(onClick = { adminVisible = !adminVisible }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (adminVisible) Icons.Default.VisibilityOff
|
imageVector = if (adminVisible) Icons.Default.VisibilityOff
|
||||||
else Icons.Default.Visibility,
|
else Icons.Default.Visibility,
|
||||||
contentDescription = if (adminVisible) "Hide" else "Show"
|
contentDescription = if (adminVisible) strings.onboardingHideAdminKey // ← "Hide"
|
||||||
|
else strings.onboardingShowAdminKey // ← "Show"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,8 +111,11 @@ fun OnboardingScreen(
|
|||||||
|
|
||||||
error?.let {
|
error?.let {
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
Text(it, color = MaterialTheme.colorScheme.error,
|
Text(
|
||||||
style = MaterialTheme.typography.bodySmall)
|
text = it,
|
||||||
|
color = MaterialTheme.colorScheme.error,
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(Modifier.height(32.dp))
|
Spacer(Modifier.height(32.dp))
|
||||||
@@ -96,7 +123,7 @@ fun OnboardingScreen(
|
|||||||
Button(
|
Button(
|
||||||
onClick = {
|
onClick = {
|
||||||
if (baseUrl.isBlank() || invoiceKey.isBlank() || adminKey.isBlank()) {
|
if (baseUrl.isBlank() || invoiceKey.isBlank() || adminKey.isBlank()) {
|
||||||
error = "All fields are required."
|
error = strings.onboardingFieldsRequired // ← "All fields are required."
|
||||||
return@Button
|
return@Button
|
||||||
}
|
}
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -106,8 +133,7 @@ fun OnboardingScreen(
|
|||||||
},
|
},
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Text("Connect")
|
Text(strings.onboardingConnect) // ← "Connect"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.bitcointxoko.gudariwallet.ui
|
package com.bitcointxoko.gudariwallet.ui
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.VisibleForTesting
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.bitcointxoko.gudariwallet.api.LnurlScanResponse
|
import com.bitcointxoko.gudariwallet.api.LnurlScanResponse
|
||||||
@@ -47,10 +46,33 @@ class WalletViewModel(
|
|||||||
val fiatCurrency: String? get() = fiatVm.fiatCurrency
|
val fiatCurrency: String? get() = fiatVm.fiatCurrency
|
||||||
val receiveState: StateFlow<ReceiveState> get() = receiveVm.receiveState
|
val receiveState: StateFlow<ReceiveState> get() = receiveVm.receiveState
|
||||||
val navigateToReceive: SharedFlow<Unit> get() = receiveVm.navigateToReceive
|
val navigateToReceive: SharedFlow<Unit> get() = receiveVm.navigateToReceive
|
||||||
fun createInvoice(amountSats: Long, memo: String) = receiveVm.createInvoice(amountSats, memo)
|
fun createInvoice(
|
||||||
|
amountSats : Long,
|
||||||
|
memo : String,
|
||||||
|
failedToCreateInvoice : String,
|
||||||
|
) = receiveVm.createInvoice(
|
||||||
|
amountSats = amountSats,
|
||||||
|
memo = memo,
|
||||||
|
failedToCreateInvoice = failedToCreateInvoice,
|
||||||
|
)
|
||||||
fun resetReceiveState() = receiveVm.resetReceiveState()
|
fun resetReceiveState() = receiveVm.resetReceiveState()
|
||||||
fun executeWithdraw(k1: String, callback: String, amountSats: Long, memo: String) =
|
fun executeWithdraw(
|
||||||
receiveVm.executeWithdraw(k1, callback, amountSats, memo)
|
k1 : String,
|
||||||
|
callback : String,
|
||||||
|
amountSats : Long,
|
||||||
|
memo : String,
|
||||||
|
failedToCreateInvoice : String,
|
||||||
|
withdrawRejected : String,
|
||||||
|
withdrawFailed : String,
|
||||||
|
) = receiveVm.executeWithdraw(
|
||||||
|
k1 = k1,
|
||||||
|
callback = callback,
|
||||||
|
amountSats = amountSats,
|
||||||
|
memo = memo,
|
||||||
|
failedToCreateInvoice = failedToCreateInvoice,
|
||||||
|
withdrawRejected = withdrawRejected,
|
||||||
|
withdrawFailed = withdrawFailed,
|
||||||
|
)
|
||||||
val sendState: StateFlow<SendState> get() = sendVm.sendState
|
val sendState: StateFlow<SendState> get() = sendVm.sendState
|
||||||
fun refreshBalance() = balanceVm.refreshBalance()
|
fun refreshBalance() = balanceVm.refreshBalance()
|
||||||
fun scan(rawInput: String) = sendVm.scan(rawInput)
|
fun scan(rawInput: String) = sendVm.scan(rawInput)
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.text.input.KeyboardType
|
import androidx.compose.ui.text.input.KeyboardType
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.bitcointxoko.gudariwallet.LocalAppStrings
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -47,6 +48,8 @@ internal fun FilterBottomSheet(
|
|||||||
onDateFilterSet : (min: Long?, max: Long?) -> Unit,
|
onDateFilterSet : (min: Long?, max: Long?) -> Unit,
|
||||||
onDateClearRequested: () -> Unit
|
onDateClearRequested: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val strings = LocalAppStrings.current
|
||||||
|
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
sheetState = sheetState
|
sheetState = sheetState
|
||||||
@@ -56,15 +59,16 @@ internal fun FilterBottomSheet(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(start = 24.dp, end = 24.dp, bottom = 32.dp)
|
.padding(start = 24.dp, end = 24.dp, bottom = 32.dp)
|
||||||
) {
|
) {
|
||||||
|
// ── Sheet title ───────────────────────────────────────────────────
|
||||||
Text(
|
Text(
|
||||||
text = "Filter payments",
|
text = strings.filterSheetTitle, // ← "Filter payments"
|
||||||
style = MaterialTheme.typography.titleMedium
|
style = MaterialTheme.typography.titleMedium
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(20.dp))
|
Spacer(Modifier.height(20.dp))
|
||||||
|
|
||||||
// ── Direction ────────────────────────────────────────────────────
|
// ── Direction ─────────────────────────────────────────────────────
|
||||||
Text(
|
Text(
|
||||||
text = "Direction",
|
text = strings.filterSectionDirection, // ← "Direction"
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
@@ -72,9 +76,9 @@ internal fun FilterBottomSheet(
|
|||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
DirectionFilter.entries.forEach { option ->
|
DirectionFilter.entries.forEach { option ->
|
||||||
val label = when (option) {
|
val label = when (option) {
|
||||||
DirectionFilter.ALL -> "All"
|
DirectionFilter.ALL -> strings.filterDirectionAll // ← "All"
|
||||||
DirectionFilter.OUTGOING -> "Outgoing"
|
DirectionFilter.OUTGOING -> strings.filterDirectionOutgoing // ← "Outgoing"
|
||||||
DirectionFilter.INCOMING -> "Incoming"
|
DirectionFilter.INCOMING -> strings.filterDirectionIncoming // ← "Incoming"
|
||||||
}
|
}
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = currentFilter.direction == option,
|
selected = currentFilter.direction == option,
|
||||||
@@ -85,9 +89,9 @@ internal fun FilterBottomSheet(
|
|||||||
}
|
}
|
||||||
Spacer(Modifier.height(20.dp))
|
Spacer(Modifier.height(20.dp))
|
||||||
|
|
||||||
// ── Status ───────────────────────────────────────────────────────
|
// ── Status ────────────────────────────────────────────────────────
|
||||||
Text(
|
Text(
|
||||||
text = "Status",
|
text = strings.filterSectionStatus, // ← "Status"
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
@@ -100,9 +104,9 @@ internal fun FilterBottomSheet(
|
|||||||
label = {
|
label = {
|
||||||
Text(
|
Text(
|
||||||
when (option) {
|
when (option) {
|
||||||
StatusFilter.COMPLETED -> "Completed"
|
StatusFilter.COMPLETED -> strings.filterStatusCompleted // ← "Completed"
|
||||||
StatusFilter.PENDING -> "Pending"
|
StatusFilter.PENDING -> strings.filterStatusPending // ← "Pending"
|
||||||
StatusFilter.FAILED -> "Failed"
|
StatusFilter.FAILED -> strings.filterStatusFailed // ← "Failed"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -110,11 +114,11 @@ internal fun FilterBottomSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Type (dynamic, only shown when tags exist) ───────────────────
|
// ── Type (dynamic, only shown when tags exist) ────────────────────
|
||||||
if (availableTypes.isNotEmpty()) {
|
if (availableTypes.isNotEmpty()) {
|
||||||
Spacer(Modifier.height(20.dp))
|
Spacer(Modifier.height(20.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Type",
|
text = strings.filterSectionType, // ← "Type"
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
@@ -131,9 +135,9 @@ internal fun FilterBottomSheet(
|
|||||||
}
|
}
|
||||||
Spacer(Modifier.height(20.dp))
|
Spacer(Modifier.height(20.dp))
|
||||||
|
|
||||||
// ── Amount (sats) ────────────────────────────────────────────────
|
// ── Amount (sats) ─────────────────────────────────────────────────
|
||||||
Text(
|
Text(
|
||||||
text = "Amount (sats)",
|
text = strings.filterSectionAmount, // ← "Amount (sats)"
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
@@ -154,8 +158,8 @@ internal fun FilterBottomSheet(
|
|||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = minText,
|
value = minText,
|
||||||
onValueChange = { minText = it.filter(Char::isDigit); commitAmount() },
|
onValueChange = { minText = it.filter(Char::isDigit); commitAmount() },
|
||||||
label = { Text("Min") },
|
label = { Text(strings.filterAmountMin_label) }, // ← "Min"
|
||||||
suffix = { Text("sats") },
|
suffix = { Text(strings.filterAmountSuffix) }, // ← "sats"
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
@@ -164,8 +168,8 @@ internal fun FilterBottomSheet(
|
|||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = maxText,
|
value = maxText,
|
||||||
onValueChange = { maxText = it.filter(Char::isDigit); commitAmount() },
|
onValueChange = { maxText = it.filter(Char::isDigit); commitAmount() },
|
||||||
label = { Text("Max") },
|
label = { Text(strings.filterAmountMax_label) }, // ← "Max"
|
||||||
suffix = { Text("sats") },
|
suffix = { Text(strings.filterAmountSuffix) }, // ← "sats"
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
@@ -173,9 +177,9 @@ internal fun FilterBottomSheet(
|
|||||||
}
|
}
|
||||||
Spacer(Modifier.height(20.dp))
|
Spacer(Modifier.height(20.dp))
|
||||||
|
|
||||||
// ── Date ─────────────────────────────────────────────────────────
|
// ── Date ──────────────────────────────────────────────────────────
|
||||||
Text(
|
Text(
|
||||||
text = "Date",
|
text = strings.filterSectionDate, // ← "Date"
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
@@ -185,9 +189,9 @@ internal fun FilterBottomSheet(
|
|||||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
DatePreset.entries.forEach { preset ->
|
DatePreset.entries.forEach { preset ->
|
||||||
val label = when (preset) {
|
val label = when (preset) {
|
||||||
DatePreset.THIS_WEEK -> "This week"
|
DatePreset.THIS_WEEK -> strings.filterDateThisWeek // ← "This week"
|
||||||
DatePreset.THIS_MONTH -> "This month"
|
DatePreset.THIS_MONTH -> strings.filterDateThisMonth // ← "This month"
|
||||||
DatePreset.THIS_YEAR -> "This year"
|
DatePreset.THIS_YEAR -> strings.filterDateThisYear // ← "This year"
|
||||||
}
|
}
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = currentFilter.datePreset == preset,
|
selected = currentFilter.datePreset == preset,
|
||||||
@@ -213,14 +217,22 @@ internal fun FilterBottomSheet(
|
|||||||
onClick = { showFromPicker = true },
|
onClick = { showFromPicker = true },
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
Text(currentFilter.minCreatedAt?.let { formatEpochShort(it) } ?: "From")
|
Text(
|
||||||
|
currentFilter.minCreatedAt
|
||||||
|
?.let { formatEpochShort(it) }
|
||||||
|
?: strings.filterDateFrom_label // ← "From"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Text("–", style = MaterialTheme.typography.bodyLarge)
|
Text("–", style = MaterialTheme.typography.bodyLarge)
|
||||||
OutlinedButton(
|
OutlinedButton(
|
||||||
onClick = { showToPicker = true },
|
onClick = { showToPicker = true },
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
) {
|
) {
|
||||||
Text(currentFilter.maxCreatedAt?.let { formatEpochShort(it) } ?: "To")
|
Text(
|
||||||
|
currentFilter.maxCreatedAt
|
||||||
|
?.let { formatEpochShort(it) }
|
||||||
|
?: strings.filterDateTo_label // ← "To"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,10 +247,12 @@ internal fun FilterBottomSheet(
|
|||||||
val epochSec = pickerState.selectedDateMillis?.div(1000)
|
val epochSec = pickerState.selectedDateMillis?.div(1000)
|
||||||
onDateFilterSet(epochSec, currentFilter.maxCreatedAt)
|
onDateFilterSet(epochSec, currentFilter.maxCreatedAt)
|
||||||
showFromPicker = false
|
showFromPicker = false
|
||||||
}) { Text("OK") }
|
}) { Text(strings.filterDialogOk) } // ← "OK"
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = { showFromPicker = false }) { Text("Cancel") }
|
TextButton(onClick = { showFromPicker = false }) {
|
||||||
|
Text(strings.filterDialogCancel) // ← "Cancel"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
) { DatePicker(state = pickerState) }
|
) { DatePicker(state = pickerState) }
|
||||||
}
|
}
|
||||||
@@ -255,10 +269,12 @@ internal fun FilterBottomSheet(
|
|||||||
val epochSec = pickerState.selectedDateMillis?.div(1000)?.let { it + 86399L }
|
val epochSec = pickerState.selectedDateMillis?.div(1000)?.let { it + 86399L }
|
||||||
onDateFilterSet(currentFilter.minCreatedAt, epochSec)
|
onDateFilterSet(currentFilter.minCreatedAt, epochSec)
|
||||||
showToPicker = false
|
showToPicker = false
|
||||||
}) { Text("OK") }
|
}) { Text(strings.filterDialogOk) } // ← "OK"
|
||||||
},
|
},
|
||||||
dismissButton = {
|
dismissButton = {
|
||||||
TextButton(onClick = { showToPicker = false }) { Text("Cancel") }
|
TextButton(onClick = { showToPicker = false }) {
|
||||||
|
Text(strings.filterDialogCancel) // ← "Cancel"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
) { DatePicker(state = pickerState) }
|
) { DatePicker(state = pickerState) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.platform.LocalClipboard
|
import androidx.compose.ui.platform.LocalClipboard
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.bitcointxoko.gudariwallet.api.PaymentRecord
|
import com.bitcointxoko.gudariwallet.api.PaymentRecord
|
||||||
|
import com.bitcointxoko.gudariwallet.LocalAppStrings
|
||||||
import com.bitcointxoko.gudariwallet.ui.HistoryState
|
import com.bitcointxoko.gudariwallet.ui.HistoryState
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@@ -68,6 +69,7 @@ fun HistoryScreen(
|
|||||||
onClose : () -> Unit,
|
onClose : () -> Unit,
|
||||||
onPaymentClick : (PaymentRecord) -> Unit
|
onPaymentClick : (PaymentRecord) -> Unit
|
||||||
) {
|
) {
|
||||||
|
val strings = LocalAppStrings.current
|
||||||
val state by vm.filteredState.collectAsState()
|
val state by vm.filteredState.collectAsState()
|
||||||
val filter by vm.filter.collectAsState()
|
val filter by vm.filter.collectAsState()
|
||||||
val fiatCurrency by vm.fiatCurrency.collectAsState()
|
val fiatCurrency by vm.fiatCurrency.collectAsState()
|
||||||
@@ -86,12 +88,12 @@ fun HistoryScreen(
|
|||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text("History") },
|
title = { Text(strings.historyTitle) }, // ← "History"
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onClose) {
|
IconButton(onClick = onClose) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Close,
|
imageVector = Icons.Default.Close,
|
||||||
contentDescription = "Close"
|
contentDescription = strings.historyClose // ← "Close"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -105,7 +107,7 @@ fun HistoryScreen(
|
|||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Search,
|
imageVector = Icons.Default.Search,
|
||||||
contentDescription = "Search payments"
|
contentDescription = strings.historySearchPayments // ← "Search payments"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,7 +117,7 @@ fun HistoryScreen(
|
|||||||
IconButton(onClick = { filterSheetVisible = true }) {
|
IconButton(onClick = { filterSheetVisible = true }) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.FilterList,
|
imageVector = Icons.Default.FilterList,
|
||||||
contentDescription = "Filter payments"
|
contentDescription = strings.historyFilterPayments // ← "Filter payments"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,8 +156,8 @@ fun HistoryScreen(
|
|||||||
val searchActive = filter.searchQuery.isNotBlank()
|
val searchActive = filter.searchQuery.isNotBlank()
|
||||||
if (isFiltered || searchActive) {
|
if (isFiltered || searchActive) {
|
||||||
ActiveFilterChips(
|
ActiveFilterChips(
|
||||||
filter = filter,
|
filter = filter,
|
||||||
searchActive = searchActive,
|
searchActive = searchActive,
|
||||||
onClearDirection = { vm.setDirectionFilter(DirectionFilter.ALL) },
|
onClearDirection = { vm.setDirectionFilter(DirectionFilter.ALL) },
|
||||||
onClearStatus = { vm.toggleStatusFilter(it) },
|
onClearStatus = { vm.toggleStatusFilter(it) },
|
||||||
onClearType = { vm.toggleTypeFilter(it) },
|
onClearType = { vm.toggleTypeFilter(it) },
|
||||||
@@ -187,7 +189,9 @@ fun HistoryScreen(
|
|||||||
style = MaterialTheme.typography.bodyMedium
|
style = MaterialTheme.typography.bodyMedium
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(12.dp))
|
Spacer(Modifier.height(12.dp))
|
||||||
Button(onClick = { vm.refresh() }) { Text("Retry") }
|
Button(onClick = { vm.refresh() }) {
|
||||||
|
Text(strings.historyRetry) // ← "Retry"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,7 +209,7 @@ fun HistoryScreen(
|
|||||||
|
|
||||||
if (s.payments.isEmpty()) {
|
if (s.payments.isEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
text = "No payments yet.",
|
text = strings.historyNoPayments, // ← "No payments yet."
|
||||||
modifier = Modifier.align(Alignment.Center),
|
modifier = Modifier.align(Alignment.Center),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
@@ -294,6 +298,7 @@ private fun SearchBar(
|
|||||||
onClear : () -> Unit,
|
onClear : () -> Unit,
|
||||||
onInfoClick : () -> Unit
|
onInfoClick : () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val strings = LocalAppStrings.current
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -303,13 +308,13 @@ private fun SearchBar(
|
|||||||
IconButton(onClick = onInfoClick) {
|
IconButton(onClick = onInfoClick) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Info,
|
imageVector = Icons.Default.Info,
|
||||||
contentDescription = "Search help"
|
contentDescription = strings.historySearchHelp // ← "Search help"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
TextField(
|
TextField(
|
||||||
value = query,
|
value = query,
|
||||||
onValueChange = onQueryChange,
|
onValueChange = onQueryChange,
|
||||||
placeholder = { Text("Search transactions") },
|
placeholder = { Text(strings.historySearchPlaceholder) }, // ← "Search transactions"
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
colors = TextFieldDefaults.colors(
|
colors = TextFieldDefaults.colors(
|
||||||
focusedContainerColor = Color.Transparent,
|
focusedContainerColor = Color.Transparent,
|
||||||
@@ -330,7 +335,7 @@ private fun SearchBar(
|
|||||||
}) {
|
}) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.ContentPaste,
|
imageVector = Icons.Default.ContentPaste,
|
||||||
contentDescription = "Paste"
|
contentDescription = strings.historySearchPaste // ← "Paste"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
@@ -339,7 +344,7 @@ private fun SearchBar(
|
|||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Close,
|
imageVector = Icons.Default.Close,
|
||||||
contentDescription = "Clear search",
|
contentDescription = strings.historySearchClear, // ← "Clear search"
|
||||||
tint = if (query.isNotEmpty()) LocalContentColor.current
|
tint = if (query.isNotEmpty()) LocalContentColor.current
|
||||||
else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f)
|
||||||
)
|
)
|
||||||
@@ -360,6 +365,7 @@ private fun ActiveFilterChips(
|
|||||||
onClearDate : () -> Unit,
|
onClearDate : () -> Unit,
|
||||||
onClearSearch : () -> Unit
|
onClearSearch : () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val strings = LocalAppStrings.current
|
||||||
FlowRow(
|
FlowRow(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -374,15 +380,15 @@ private fun ActiveFilterChips(
|
|||||||
label = {
|
label = {
|
||||||
Text(
|
Text(
|
||||||
when (filter.direction) {
|
when (filter.direction) {
|
||||||
DirectionFilter.OUTGOING -> "Outgoing"
|
DirectionFilter.OUTGOING -> strings.filterDirectionOutgoing // ← "Outgoing"
|
||||||
else -> "Incoming"
|
else -> strings.filterDirectionIncoming // ← "Incoming"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Close,
|
imageVector = Icons.Default.Close,
|
||||||
contentDescription = "Clear direction filter",
|
contentDescription = strings.filterClearDirection, // ← "Clear direction filter"
|
||||||
modifier = Modifier.size(16.dp)
|
modifier = Modifier.size(16.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -395,16 +401,16 @@ private fun ActiveFilterChips(
|
|||||||
label = {
|
label = {
|
||||||
Text(
|
Text(
|
||||||
when (s) {
|
when (s) {
|
||||||
StatusFilter.COMPLETED -> "Completed"
|
StatusFilter.COMPLETED -> strings.filterStatusCompleted // ← "Completed"
|
||||||
StatusFilter.PENDING -> "Pending"
|
StatusFilter.PENDING -> strings.filterStatusPending // ← "Pending"
|
||||||
StatusFilter.FAILED -> "Failed"
|
StatusFilter.FAILED -> strings.filterStatusFailed // ← "Failed"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Close,
|
imageVector = Icons.Default.Close,
|
||||||
contentDescription = "Remove status filter",
|
contentDescription = strings.filterRemoveStatus, // ← "Remove status filter"
|
||||||
modifier = Modifier.size(16.dp)
|
modifier = Modifier.size(16.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -418,7 +424,7 @@ private fun ActiveFilterChips(
|
|||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Close,
|
imageVector = Icons.Default.Close,
|
||||||
contentDescription = "Remove type filter",
|
contentDescription = strings.filterRemoveType, // ← "Remove type filter"
|
||||||
modifier = Modifier.size(16.dp)
|
modifier = Modifier.size(16.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -427,11 +433,11 @@ private fun ActiveFilterChips(
|
|||||||
if (filter.minAmountSat != null || filter.maxAmountSat != null) {
|
if (filter.minAmountSat != null || filter.maxAmountSat != null) {
|
||||||
val label = when {
|
val label = when {
|
||||||
filter.minAmountSat != null && filter.maxAmountSat != null ->
|
filter.minAmountSat != null && filter.maxAmountSat != null ->
|
||||||
"${filter.minAmountSat}–${filter.maxAmountSat} sats"
|
strings.filterAmountRange(filter.minAmountSat, filter.maxAmountSat) // ← "X–Y sats"
|
||||||
filter.minAmountSat != null ->
|
filter.minAmountSat != null ->
|
||||||
"≥ ${filter.minAmountSat} sats"
|
strings.filterAmountMin(filter.minAmountSat) // ← "≥ X sats"
|
||||||
else ->
|
else ->
|
||||||
"≤ ${filter.maxAmountSat} sats"
|
strings.filterAmountMax(filter.maxAmountSat!!) // ← "≤ X sats"
|
||||||
}
|
}
|
||||||
InputChip(
|
InputChip(
|
||||||
selected = true,
|
selected = true,
|
||||||
@@ -440,7 +446,7 @@ private fun ActiveFilterChips(
|
|||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Close,
|
imageVector = Icons.Default.Close,
|
||||||
contentDescription = "Clear amount filter",
|
contentDescription = strings.filterClearAmount, // ← "Clear amount filter"
|
||||||
modifier = Modifier.size(16.dp)
|
modifier = Modifier.size(16.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -450,16 +456,19 @@ private fun ActiveFilterChips(
|
|||||||
val maxCreatedAt = filter.maxCreatedAt
|
val maxCreatedAt = filter.maxCreatedAt
|
||||||
if (minCreatedAt != null || maxCreatedAt != null) {
|
if (minCreatedAt != null || maxCreatedAt != null) {
|
||||||
val label = when (filter.datePreset) {
|
val label = when (filter.datePreset) {
|
||||||
DatePreset.THIS_WEEK -> "This week"
|
DatePreset.THIS_WEEK -> strings.filterDateThisWeek // ← "This week"
|
||||||
DatePreset.THIS_MONTH -> "This month"
|
DatePreset.THIS_MONTH -> strings.filterDateThisMonth // ← "This month"
|
||||||
DatePreset.THIS_YEAR -> "This year"
|
DatePreset.THIS_YEAR -> strings.filterDateThisYear // ← "This year"
|
||||||
null -> when {
|
null -> when {
|
||||||
minCreatedAt != null && maxCreatedAt != null ->
|
minCreatedAt != null && maxCreatedAt != null ->
|
||||||
"${formatEpochShort(minCreatedAt)}–${formatEpochShort(maxCreatedAt)}"
|
strings.filterDateRange( // ← "X–Y"
|
||||||
|
formatEpochShort(minCreatedAt),
|
||||||
|
formatEpochShort(maxCreatedAt)
|
||||||
|
)
|
||||||
minCreatedAt != null ->
|
minCreatedAt != null ->
|
||||||
"From ${formatEpochShort(minCreatedAt)}"
|
strings.filterDateFrom(formatEpochShort(minCreatedAt)) // ← "From X"
|
||||||
else ->
|
else ->
|
||||||
"Until ${formatEpochShort(maxCreatedAt!!)}"
|
strings.filterDateUntil(formatEpochShort(maxCreatedAt!!)) // ← "Until X"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
InputChip(
|
InputChip(
|
||||||
@@ -469,7 +478,7 @@ private fun ActiveFilterChips(
|
|||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Close,
|
imageVector = Icons.Default.Close,
|
||||||
contentDescription = "Clear date filter",
|
contentDescription = strings.filterClearDate, // ← "Clear date filter"
|
||||||
modifier = Modifier.size(16.dp)
|
modifier = Modifier.size(16.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -479,11 +488,11 @@ private fun ActiveFilterChips(
|
|||||||
InputChip(
|
InputChip(
|
||||||
selected = true,
|
selected = true,
|
||||||
onClick = onClearSearch,
|
onClick = onClearSearch,
|
||||||
label = { Text("search: …${filter.searchQuery.takeLast(8)}") },
|
label = { Text(strings.historySearchChip(filter.searchQuery.takeLast(8))) }, // ← "search: …XXXXXXXX"
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Close,
|
imageVector = Icons.Default.Close,
|
||||||
contentDescription = "Clear search",
|
contentDescription = strings.historyClearSearch, // ← "Clear search"
|
||||||
modifier = Modifier.size(16.dp)
|
modifier = Modifier.size(16.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.bitcointxoko.gudariwallet.api.PaymentRecord
|
import com.bitcointxoko.gudariwallet.api.PaymentRecord
|
||||||
|
import com.bitcointxoko.gudariwallet.LocalAppStrings
|
||||||
import com.bitcointxoko.gudariwallet.ui.DetailState
|
import com.bitcointxoko.gudariwallet.ui.DetailState
|
||||||
import com.bitcointxoko.gudariwallet.util.feePpm
|
import com.bitcointxoko.gudariwallet.util.feePpm
|
||||||
import com.bitcointxoko.gudariwallet.util.formatFiatForSats
|
import com.bitcointxoko.gudariwallet.util.formatFiatForSats
|
||||||
@@ -57,6 +58,7 @@ fun PaymentDetailScreen(
|
|||||||
vm : HistoryViewModel,
|
vm : HistoryViewModel,
|
||||||
onBack : () -> Unit
|
onBack : () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val strings = LocalAppStrings.current
|
||||||
val detailState by vm.detailState.collectAsState()
|
val detailState by vm.detailState.collectAsState()
|
||||||
val fiatCurrency by vm.fiatCurrency.collectAsState()
|
val fiatCurrency by vm.fiatCurrency.collectAsState()
|
||||||
val fiatSatsPerUnit by vm.fiatSatsPerUnit.collectAsState()
|
val fiatSatsPerUnit by vm.fiatSatsPerUnit.collectAsState()
|
||||||
@@ -82,10 +84,18 @@ fun PaymentDetailScreen(
|
|||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = { Text(if (payment.isOutgoing) "Outgoing payment" else "Incoming payment") },
|
title = {
|
||||||
|
Text(
|
||||||
|
if (payment.isOutgoing) strings.detailOutgoingTitle // ← "Outgoing payment"
|
||||||
|
else strings.detailIncomingTitle // ← "Incoming payment"
|
||||||
|
)
|
||||||
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onBack) {
|
IconButton(onClick = onBack) {
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = strings.detailBack // ← "Back"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -106,7 +116,7 @@ fun PaymentDetailScreen(
|
|||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Could not load full details",
|
text = strings.detailCouldNotLoad, // ← "Could not load full details"
|
||||||
color = MaterialTheme.colorScheme.onErrorContainer,
|
color = MaterialTheme.colorScheme.onErrorContainer,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
modifier = Modifier.padding(12.dp)
|
modifier = Modifier.padding(12.dp)
|
||||||
@@ -141,6 +151,7 @@ private fun PaymentDetailContent(
|
|||||||
fiatSatsPerUnit : Double?,
|
fiatSatsPerUnit : Double?,
|
||||||
modifier : Modifier = Modifier
|
modifier : Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
val strings = LocalAppStrings.current
|
||||||
val amountColor = paymentAmountColor(payment.status, payment.isOutgoing)
|
val amountColor = paymentAmountColor(payment.status, payment.isOutgoing)
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
@@ -195,11 +206,12 @@ private fun PaymentDetailContent(
|
|||||||
amountMsat = kotlin.math.abs(payment.amountMsat),
|
amountMsat = kotlin.math.abs(payment.amountMsat),
|
||||||
feeMsat = kotlin.math.abs(payment.feeMsat)
|
feeMsat = kotlin.math.abs(payment.feeMsat)
|
||||||
)
|
)
|
||||||
val feeLabel = buildString {
|
// Build the optional parts separately so the lambda stays clean
|
||||||
append("Fee: ${payment.feeSat} sats")
|
val fiatPart = if (feeFiat != null) " ($feeFiat)" else ""
|
||||||
if (feeFiat != null) append(" ($feeFiat)")
|
val ppmPart = if (ppm != null) " · $ppm ppm" else ""
|
||||||
if (ppm != null) append(" · $ppm ppm")
|
val feeLabel = strings.detailFeeLabel( // ← "Fee: X sats (fiat) · Y ppm"
|
||||||
}
|
payment.feeSat, fiatPart, ppmPart
|
||||||
|
)
|
||||||
Spacer(Modifier.height(4.dp))
|
Spacer(Modifier.height(4.dp))
|
||||||
Text(
|
Text(
|
||||||
text = feeLabel,
|
text = feeLabel,
|
||||||
@@ -215,14 +227,17 @@ private fun PaymentDetailContent(
|
|||||||
|
|
||||||
// ── Basic info ───────────────────────────────────────────────────────
|
// ── Basic info ───────────────────────────────────────────────────────
|
||||||
item {
|
item {
|
||||||
DetailSection(title = "Details") {
|
DetailSection(title = strings.detailSectionDetails) { // ← "Details"
|
||||||
DetailRow("Date", formatTimestamp(payment.createdAt, payment.time))
|
DetailRow(strings.detailRowDate, formatTimestamp(payment.createdAt, payment.time)) // ← "Date"
|
||||||
DetailRow("Memo", payment.memo?.takeIf { it.isNotBlank() } ?: "—")
|
DetailRow(
|
||||||
|
strings.detailRowMemo, // ← "Memo"
|
||||||
|
payment.memo?.takeIf { it.isNotBlank() } ?: strings.detailRowMemoEmpty // ← "—"
|
||||||
|
)
|
||||||
if (payment.extra?.tag != null) {
|
if (payment.extra?.tag != null) {
|
||||||
DetailRow("Type", payment.extra.tag)
|
DetailRow(strings.detailRowType, payment.extra.tag) // ← "Type"
|
||||||
}
|
}
|
||||||
if (payment.extra?.comment?.isNotBlank() == true) {
|
if (payment.extra?.comment?.isNotBlank() == true) {
|
||||||
DetailRow("Comment", payment.extra.comment)
|
DetailRow(strings.detailRowComment, payment.extra.comment) // ← "Comment"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -230,20 +245,20 @@ private fun PaymentDetailContent(
|
|||||||
// ── Success action (LNURL) ───────────────────────────────────────────
|
// ── Success action (LNURL) ───────────────────────────────────────────
|
||||||
payment.extra?.successAction?.let { action ->
|
payment.extra?.successAction?.let { action ->
|
||||||
item {
|
item {
|
||||||
DetailSection(title = "Success Action") {
|
DetailSection(title = strings.detailSectionSuccessAction) { // ← "Success Action"
|
||||||
action.message?.let { DetailRow("Message", it) }
|
action.message?.let { DetailRow(strings.detailSuccessMessage, it) } // ← "Message"
|
||||||
action.url?.let { DetailRow("URL", it) }
|
action.url?.let { DetailRow(strings.detailSuccessUrl, it) } // ← "URL"
|
||||||
action.description?.let { DetailRow("Description", it) }
|
action.description?.let { DetailRow(strings.detailSuccessDescription, it) } // ← "Description"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Technical ────────────────────────────────────────────────────────
|
// ── Technical ────────────────────────────────────────────────────────
|
||||||
item {
|
item {
|
||||||
DetailSection(title = "Technical") {
|
DetailSection(title = strings.detailSectionTechnical) { // ← "Technical"
|
||||||
DetailRow(
|
DetailRow(
|
||||||
label = "Payment Hash",
|
label = strings.detailRowPaymentHash, // ← "Payment Hash"
|
||||||
value = payment.paymentHash,
|
value = payment.paymentHash,
|
||||||
monospace = true,
|
monospace = true,
|
||||||
copyable = true,
|
copyable = true,
|
||||||
onCopy = {
|
onCopy = {
|
||||||
@@ -256,8 +271,8 @@ private fun PaymentDetailContent(
|
|||||||
)
|
)
|
||||||
if (!payment.preimage.isNullOrBlank() && payment.preimage != "0".repeat(64)) {
|
if (!payment.preimage.isNullOrBlank() && payment.preimage != "0".repeat(64)) {
|
||||||
DetailRow(
|
DetailRow(
|
||||||
label = "Preimage",
|
label = strings.detailRowPreimage, // ← "Preimage"
|
||||||
value = payment.preimage,
|
value = payment.preimage,
|
||||||
monospace = true,
|
monospace = true,
|
||||||
copyable = true,
|
copyable = true,
|
||||||
onCopy = {
|
onCopy = {
|
||||||
@@ -273,11 +288,11 @@ private fun PaymentDetailContent(
|
|||||||
val ambossUrl = "https://amboss.space/node/$pubkey"
|
val ambossUrl = "https://amboss.space/node/$pubkey"
|
||||||
val label = alias ?: "${pubkey.take(8)}…${pubkey.takeLast(8)}"
|
val label = alias ?: "${pubkey.take(8)}…${pubkey.takeLast(8)}"
|
||||||
DetailRow(
|
DetailRow(
|
||||||
label = "Destination",
|
label = strings.detailRowDestination, // ← "Destination"
|
||||||
value = label,
|
value = label,
|
||||||
valueColor = MaterialTheme.colorScheme.primary,
|
valueColor = MaterialTheme.colorScheme.primary,
|
||||||
textDecoration = androidx.compose.ui.text.style.TextDecoration.Underline,
|
textDecoration = androidx.compose.ui.text.style.TextDecoration.Underline,
|
||||||
onValueClick = {
|
onValueClick = {
|
||||||
context.startActivity(Intent(Intent.ACTION_VIEW, ambossUrl.toUri()))
|
context.startActivity(Intent(Intent.ACTION_VIEW, ambossUrl.toUri()))
|
||||||
},
|
},
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
@@ -293,7 +308,7 @@ private fun PaymentDetailContent(
|
|||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.ContentCopy,
|
imageVector = Icons.Outlined.ContentCopy,
|
||||||
contentDescription = "Copy node ID",
|
contentDescription = strings.detailCopyNodeId, // ← "Copy node ID"
|
||||||
modifier = Modifier.size(14.dp),
|
modifier = Modifier.size(14.dp),
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
@@ -303,8 +318,8 @@ private fun PaymentDetailContent(
|
|||||||
}
|
}
|
||||||
if (!payment.bolt11.isNullOrBlank()) {
|
if (!payment.bolt11.isNullOrBlank()) {
|
||||||
DetailRow(
|
DetailRow(
|
||||||
label = "BOLT11",
|
label = strings.detailRowBolt11, // ← "BOLT11"
|
||||||
value = payment.bolt11,
|
value = payment.bolt11,
|
||||||
monospace = true,
|
monospace = true,
|
||||||
copyable = true,
|
copyable = true,
|
||||||
maxLines = 3,
|
maxLines = 3,
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import androidx.compose.ui.Modifier
|
|||||||
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 com.bitcointxoko.gudariwallet.api.PaymentRecord
|
import com.bitcointxoko.gudariwallet.api.PaymentRecord
|
||||||
|
import com.bitcointxoko.gudariwallet.LocalAppStrings
|
||||||
import com.bitcointxoko.gudariwallet.ui.theme.semanticColors
|
import com.bitcointxoko.gudariwallet.ui.theme.semanticColors
|
||||||
import com.bitcointxoko.gudariwallet.util.formatFiatForSats
|
import com.bitcointxoko.gudariwallet.util.formatFiatForSats
|
||||||
|
|
||||||
@@ -33,8 +34,9 @@ internal fun PaymentRow(
|
|||||||
fiatSatsPerUnit : Double?,
|
fiatSatsPerUnit : Double?,
|
||||||
onClick : () -> Unit
|
onClick : () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val strings = LocalAppStrings.current
|
||||||
val isPending = isPendingStatus(payment.status)
|
val isPending = isPendingStatus(payment.status)
|
||||||
val semantic = semanticColors()
|
val semantic = semanticColors()
|
||||||
val amountColor = paymentAmountColor(payment.status, payment.isOutgoing)
|
val amountColor = paymentAmountColor(payment.status, payment.isOutgoing)
|
||||||
val icon = if (payment.isOutgoing) Icons.Default.ArrowUpward
|
val icon = if (payment.isOutgoing) Icons.Default.ArrowUpward
|
||||||
else Icons.Default.ArrowDownward
|
else Icons.Default.ArrowDownward
|
||||||
@@ -54,7 +56,8 @@ internal fun PaymentRow(
|
|||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = if (payment.isOutgoing) "Sent" else "Received",
|
contentDescription = if (payment.isOutgoing) strings.paymentRowSentCD // ← "Sent"
|
||||||
|
else strings.paymentRowReceivedCD, // ← "Received"
|
||||||
tint = amountColor,
|
tint = amountColor,
|
||||||
modifier = Modifier.size(20.dp)
|
modifier = Modifier.size(20.dp)
|
||||||
)
|
)
|
||||||
@@ -62,7 +65,8 @@ internal fun PaymentRow(
|
|||||||
|
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text(
|
Text(
|
||||||
text = payment.memo?.takeIf { it.isNotBlank() } ?: "No memo",
|
text = payment.memo?.takeIf { it.isNotBlank() }
|
||||||
|
?: strings.paymentRowNoMemo, // ← "No memo"
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
@@ -84,7 +88,7 @@ internal fun PaymentRow(
|
|||||||
contentColor = semantic.onWarningContainer
|
contentColor = semantic.onWarningContainer
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "Pending",
|
text = strings.paymentRowPending, // ← "Pending"
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.labelSmall,
|
||||||
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
|
modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import androidx.compose.material3.rememberModalBottomSheetState
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.bitcointxoko.gudariwallet.LocalAppStrings
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -20,6 +21,8 @@ internal fun SearchInfoSheet(visible: Boolean, onDismiss: () -> Unit) {
|
|||||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||||
if (!visible) return
|
if (!visible) return
|
||||||
|
|
||||||
|
val strings = LocalAppStrings.current
|
||||||
|
|
||||||
ModalBottomSheet(
|
ModalBottomSheet(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
sheetState = sheetState
|
sheetState = sheetState
|
||||||
@@ -30,55 +33,45 @@ internal fun SearchInfoSheet(visible: Boolean, onDismiss: () -> Unit) {
|
|||||||
.padding(start = 24.dp, end = 24.dp, bottom = 40.dp)
|
.padding(start = 24.dp, end = 24.dp, bottom = 40.dp)
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = "What can I search for?",
|
text = strings.searchInfoTitle, // ← "What can I search for?"
|
||||||
style = MaterialTheme.typography.titleMedium
|
style = MaterialTheme.typography.titleMedium
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Type any part of the following to find a payment:",
|
text = strings.searchInfoSubtitle, // ← "Type any part of the following…"
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
Spacer(Modifier.height(20.dp))
|
Spacer(Modifier.height(20.dp))
|
||||||
|
|
||||||
SearchInfoItem(
|
SearchInfoItem(
|
||||||
title = "Memo",
|
title = strings.searchInfoMemoTitle, // ← "Memo"
|
||||||
description = "The message or description attached to a payment. " +
|
description = strings.searchInfoMemoDesc
|
||||||
"For example, \"coffee\" or \"rent March\"."
|
|
||||||
)
|
)
|
||||||
SearchInfoItem(
|
SearchInfoItem(
|
||||||
title = "Invoice",
|
title = strings.searchInfoInvoiceTitle, // ← "Invoice"
|
||||||
description = "The payment request you scanned or pasted to send " +
|
description = strings.searchInfoInvoiceDesc
|
||||||
"a payment. It usually starts with \"lnbc…\". " +
|
|
||||||
"You can paste just the first or last few characters."
|
|
||||||
)
|
)
|
||||||
SearchInfoItem(
|
SearchInfoItem(
|
||||||
title = "Hash",
|
title = strings.searchInfoHashTitle, // ← "Hash"
|
||||||
description = "A unique code that identifies this payment on the " +
|
description = strings.searchInfoHashDesc
|
||||||
"Lightning Network. Useful if someone asks you to " +
|
|
||||||
"confirm a specific transaction."
|
|
||||||
)
|
)
|
||||||
SearchInfoItem(
|
SearchInfoItem(
|
||||||
title = "Preimage",
|
title = strings.searchInfoPreimageTitle, // ← "Preimage"
|
||||||
description = "A secret code that proves a payment was received. " +
|
description = strings.searchInfoPreimageDesc
|
||||||
"Only available after a payment completes successfully."
|
|
||||||
)
|
)
|
||||||
SearchInfoItem(
|
SearchInfoItem(
|
||||||
title = "Node alias",
|
title = strings.searchInfoNodeAliasTitle, // ← "Node alias"
|
||||||
description = "The name of the Lightning node you sent a payment to. " +
|
description = strings.searchInfoNodeAliasDesc
|
||||||
"For example, \"ACINQ\" or \"Wallet of Satoshi\". " +
|
|
||||||
"Aliases are resolved automatically for outgoing payments."
|
|
||||||
)
|
)
|
||||||
SearchInfoItem(
|
SearchInfoItem(
|
||||||
title = "Node ID (pubkey)",
|
title = strings.searchInfoNodeIdTitle, // ← "Node ID (pubkey)"
|
||||||
description = "The public key of the destination node. " +
|
description = strings.searchInfoNodeIdDesc
|
||||||
"A 66-character hex string — you can paste just the first few characters."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(Modifier.height(16.dp))
|
Spacer(Modifier.height(16.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "You can paste a full value or search with just a few characters — " +
|
text = strings.searchInfoFooter, // ← "You can paste a full value…"
|
||||||
"partial matches work too.",
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,19 +9,23 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.bitcointxoko.gudariwallet.LocalAppStrings
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Displays a sats amount in bold with an optional fiat subtitle.
|
* Displays a sats amount in bold with an optional fiat subtitle.
|
||||||
|
* Uses singular "sat" when amountSats == 1, plural "sats" otherwise.
|
||||||
* Used in MemoStepContent, ReceiveInvoiceContent, and ReceiveSuccessContent.
|
* Used in MemoStepContent, ReceiveInvoiceContent, and ReceiveSuccessContent.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
internal fun AmountDisplay(
|
internal fun AmountDisplay(
|
||||||
amountSats : Long,
|
amountSats : Long,
|
||||||
fiatLabel : String?,
|
fiatLabel : String?,
|
||||||
style : TextStyle = MaterialTheme.typography.titleLarge
|
style : TextStyle = MaterialTheme.typography.titleLarge
|
||||||
) {
|
) {
|
||||||
|
val strings = LocalAppStrings.current
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "$amountSats sats",
|
text = strings.amountDisplaySats(amountSats), // ← "1 sat" or "X sats"
|
||||||
style = style,
|
style = style,
|
||||||
fontWeight = FontWeight.Bold
|
fontWeight = FontWeight.Bold
|
||||||
)
|
)
|
||||||
|
|||||||
+5
-2
@@ -7,18 +7,21 @@ import androidx.compose.material3.Icon
|
|||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import com.bitcointxoko.gudariwallet.LocalAppStrings
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun BrightnessToggleButton(
|
internal fun BrightnessToggleButton(
|
||||||
isOn : Boolean,
|
isOn : Boolean,
|
||||||
onToggle: () -> Unit
|
onToggle: () -> Unit
|
||||||
) {
|
) {
|
||||||
|
val strings = LocalAppStrings.current
|
||||||
|
|
||||||
IconButton(onClick = onToggle) {
|
IconButton(onClick = onToggle) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (isOn) Icons.Filled.BrightnessHigh
|
imageVector = if (isOn) Icons.Filled.BrightnessHigh
|
||||||
else Icons.Filled.BrightnessLow,
|
else Icons.Filled.BrightnessLow,
|
||||||
contentDescription = if (isOn) "Reduce brightness"
|
contentDescription = if (isOn) strings.brightnessReduce // ← "Reduce brightness"
|
||||||
else "Boost brightness",
|
else strings.brightnessBoost, // ← "Boost brightness"
|
||||||
tint = if (isOn) MaterialTheme.colorScheme.primary
|
tint = if (isOn) MaterialTheme.colorScheme.primary
|
||||||
else MaterialTheme.colorScheme.onSurfaceVariant
|
else MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.graphics.createBitmap
|
import androidx.core.graphics.createBitmap
|
||||||
import androidx.core.graphics.set
|
import androidx.core.graphics.set
|
||||||
|
import com.bitcointxoko.gudariwallet.LocalAppStrings
|
||||||
import com.bitcointxoko.gudariwallet.util.IntentUtils
|
import com.bitcointxoko.gudariwallet.util.IntentUtils
|
||||||
import com.google.zxing.BarcodeFormat
|
import com.google.zxing.BarcodeFormat
|
||||||
import com.google.zxing.qrcode.QRCodeWriter
|
import com.google.zxing.qrcode.QRCodeWriter
|
||||||
@@ -69,6 +70,7 @@ internal fun QrDisplayCard(
|
|||||||
nfcEnabled : Boolean = true,
|
nfcEnabled : Boolean = true,
|
||||||
qrModifier : Modifier = Modifier.size(260.dp)
|
qrModifier : Modifier = Modifier.size(260.dp)
|
||||||
) {
|
) {
|
||||||
|
val strings = LocalAppStrings.current
|
||||||
val bitmap = remember(content) { generateQrBitmap(content).asImageBitmap() }
|
val bitmap = remember(content) { generateQrBitmap(content).asImageBitmap() }
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val clipboard = LocalClipboard.current
|
val clipboard = LocalClipboard.current
|
||||||
@@ -79,7 +81,7 @@ internal fun QrDisplayCard(
|
|||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
// ── QR image ─────────────────────────────────────────────────────────
|
// ── QR image ──────────────────────────────────────────────────────────
|
||||||
Image(
|
Image(
|
||||||
bitmap = bitmap,
|
bitmap = bitmap,
|
||||||
contentDescription = contentDescription,
|
contentDescription = contentDescription,
|
||||||
@@ -101,7 +103,7 @@ internal fun QrDisplayCard(
|
|||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.Share,
|
imageVector = Icons.Filled.Share,
|
||||||
contentDescription = "Share",
|
contentDescription = strings.qrShare, // ← "Share"
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -118,34 +120,35 @@ internal fun QrDisplayCard(
|
|||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.ContentCopy,
|
imageVector = Icons.Filled.ContentCopy,
|
||||||
contentDescription = "Copy",
|
contentDescription = strings.qrCopy, // ← "Copy"
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Brightness
|
// Brightness
|
||||||
FilledTonalIconButton(
|
FilledTonalIconButton(onClick = onToggleBrightness) {
|
||||||
onClick = onToggleBrightness
|
|
||||||
) {
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Filled.WbSunny,
|
imageVector = Icons.Filled.WbSunny,
|
||||||
contentDescription = if (brightnessOn) "Reduce brightness" else "Boost brightness",
|
contentDescription = if (brightnessOn) strings.brightnessReduce // ← "Reduce brightness"
|
||||||
|
else strings.brightnessBoost, // ← "Boost brightness"
|
||||||
tint = if (brightnessOn) MaterialTheme.colorScheme.primary
|
tint = if (brightnessOn) MaterialTheme.colorScheme.primary
|
||||||
else MaterialTheme.colorScheme.onSurfaceVariant
|
else MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NFC — only shown when caller passes isNfcEmulating
|
// NFC — only shown when caller passes isNfcEmulating
|
||||||
if (isNfcEmulating != null && onToggleNfc != null) {
|
if (isNfcEmulating != null && onToggleNfc != null) {
|
||||||
FilledTonalIconButton(
|
FilledTonalIconButton(
|
||||||
onClick = onToggleNfc,
|
onClick = onToggleNfc,
|
||||||
enabled = nfcEnabled
|
enabled = nfcEnabled
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (isNfcEmulating) Icons.Filled.Nfc else Icons.Outlined.Nfc,
|
imageVector = if (isNfcEmulating) Icons.Filled.Nfc
|
||||||
contentDescription = if (isNfcEmulating) "Stop NFC sharing" else "Share via NFC",
|
else Icons.Outlined.Nfc,
|
||||||
|
contentDescription = if (isNfcEmulating) strings.qrNfcStop // ← "Stop NFC sharing"
|
||||||
|
else strings.qrNfcStart, // ← "Share via NFC"
|
||||||
tint = if (isNfcEmulating) MaterialTheme.colorScheme.primary
|
tint = if (isNfcEmulating) MaterialTheme.colorScheme.primary
|
||||||
else MaterialTheme.colorScheme.onSurfaceVariant
|
else MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -153,7 +156,7 @@ internal fun QrDisplayCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── QR generation ────────────────────────────────────────────────────────────
|
// ── QR generation ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private const val QR_BITMAP_SIZE = 512
|
private const val QR_BITMAP_SIZE = 512
|
||||||
|
|
||||||
|
|||||||
@@ -133,7 +133,11 @@ fun ReceiveScreen(
|
|||||||
onToggleBrightness = onToggleBrightness,
|
onToggleBrightness = onToggleBrightness,
|
||||||
isLoading = receiveState is ReceiveState.AwaitingInvoice,
|
isLoading = receiveState is ReceiveState.AwaitingInvoice,
|
||||||
onCreateInvoice = { amountSats, memo ->
|
onCreateInvoice = { amountSats, memo ->
|
||||||
vm.createInvoice(amountSats, memo)
|
vm.createInvoice(
|
||||||
|
amountSats,
|
||||||
|
memo,
|
||||||
|
failedToCreateInvoice = strings.receiveVmFailedToCreateInvoice
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -146,7 +150,10 @@ fun ReceiveScreen(
|
|||||||
k1 = state.k1,
|
k1 = state.k1,
|
||||||
callback = state.callback,
|
callback = state.callback,
|
||||||
amountSats = amountSats,
|
amountSats = amountSats,
|
||||||
memo = state.defaultDescription
|
memo = state.defaultDescription,
|
||||||
|
failedToCreateInvoice = strings.receiveVmFailedToCreateInvoice,
|
||||||
|
withdrawRejected = strings.receiveVmWithdrawRejected,
|
||||||
|
withdrawFailed = strings.receiveVmWithdrawFailed,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onDismiss = { vm.resetReceiveState() }
|
onDismiss = { vm.resetReceiveState() }
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ class ReceiveViewModel(
|
|||||||
private val repo: WalletRepository,
|
private val repo: WalletRepository,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
private val _receiveState = MutableStateFlow<ReceiveState>(ReceiveState.Idle)
|
private val _receiveState =
|
||||||
|
MutableStateFlow<ReceiveState>(ReceiveState.Idle)
|
||||||
val receiveState: StateFlow<ReceiveState> = _receiveState
|
val receiveState: StateFlow<ReceiveState> = _receiveState
|
||||||
|
|
||||||
private val _navigateToReceive = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
|
private val _navigateToReceive = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
|
||||||
@@ -40,8 +41,16 @@ class ReceiveViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Invoice creation ─────────────────────────────────────────────────────
|
// ── Invoice creation ──────────────────────────────────────────────────────
|
||||||
fun createInvoice(amountSats: Long, memo: String) {
|
/**
|
||||||
|
* @param failedToCreateInvoice Localised error string from the call site,
|
||||||
|
* e.g. `strings.receiveVmFailedToCreateInvoice`
|
||||||
|
*/
|
||||||
|
fun createInvoice(
|
||||||
|
amountSats : Long,
|
||||||
|
memo : String,
|
||||||
|
failedToCreateInvoice : String, // ← injected from Composable
|
||||||
|
) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_receiveState.value = ReceiveState.AwaitingInvoice
|
_receiveState.value = ReceiveState.AwaitingInvoice
|
||||||
runCatching { repo.createInvoice(amountSats, memo) }
|
runCatching { repo.createInvoice(amountSats, memo) }
|
||||||
@@ -64,17 +73,21 @@ class ReceiveViewModel(
|
|||||||
}
|
}
|
||||||
.onFailure { e ->
|
.onFailure { e ->
|
||||||
Log.e(TAG, "RECEIVE [CREATE INVOICE ERROR] ${e.message}")
|
Log.e(TAG, "RECEIVE [CREATE INVOICE ERROR] ${e.message}")
|
||||||
_receiveState.value = ReceiveState.Error(parseApiError(e, "Failed to create invoice"))
|
_receiveState.value = ReceiveState.Error(
|
||||||
|
parseApiError(e, failedToCreateInvoice) // ← localised
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resetReceiveState() { _receiveState.value = ReceiveState.Idle }
|
fun resetReceiveState() { _receiveState.value = ReceiveState.Idle }
|
||||||
|
|
||||||
// ── LNURL-withdraw ───────────────────────────────────────────────────────
|
// ── LNURL-withdraw ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// Called from WalletViewModel's scan handlers when a withdrawRequest is detected.
|
fun handleWithdrawResponse(
|
||||||
fun handleWithdrawResponse(raw: com.bitcointxoko.gudariwallet.api.LnurlScanResponse, lnurl: String) {
|
raw : com.bitcointxoko.gudariwallet.api.LnurlScanResponse,
|
||||||
|
lnurl : String
|
||||||
|
) {
|
||||||
val minSats = (raw.minWithdrawable ?: 0L) / WalletConstants.MSAT_PER_SAT
|
val minSats = (raw.minWithdrawable ?: 0L) / WalletConstants.MSAT_PER_SAT
|
||||||
val maxSats = (raw.maxWithdrawable ?: 0L) / WalletConstants.MSAT_PER_SAT
|
val maxSats = (raw.maxWithdrawable ?: 0L) / WalletConstants.MSAT_PER_SAT
|
||||||
_receiveState.value = ReceiveState.LnurlWithdrawReady(
|
_receiveState.value = ReceiveState.LnurlWithdrawReady(
|
||||||
@@ -90,20 +103,31 @@ class ReceiveViewModel(
|
|||||||
Log.d(TAG, "LNURL-WITHDRAW [NAV EVENT] min=$minSats max=$maxSats sats — navigating to Receive")
|
Log.d(TAG, "LNURL-WITHDRAW [NAV EVENT] min=$minSats max=$maxSats sats — navigating to Receive")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param failedToCreateInvoice `strings.receiveVmFailedToCreateInvoice`
|
||||||
|
* @param withdrawRejected `strings.receiveVmWithdrawRejected`
|
||||||
|
* @param withdrawFailed `strings.receiveVmWithdrawFailed`
|
||||||
|
*/
|
||||||
fun executeWithdraw(
|
fun executeWithdraw(
|
||||||
k1 : String,
|
k1 : String,
|
||||||
callback : String,
|
callback : String,
|
||||||
amountSats: Long,
|
amountSats : Long,
|
||||||
memo : String
|
memo : String,
|
||||||
|
failedToCreateInvoice : String, // ← injected from Composable
|
||||||
|
withdrawRejected : String, // ← injected from Composable
|
||||||
|
withdrawFailed : String, // ← injected from Composable
|
||||||
) {
|
) {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_receiveState.value = ReceiveState.AwaitingInvoice
|
_receiveState.value = ReceiveState.AwaitingInvoice
|
||||||
|
|
||||||
val invoiceResult = runCatching { repo.createInvoice(amountSats, memo) }
|
val invoiceResult = runCatching { repo.createInvoice(amountSats, memo) }
|
||||||
if (invoiceResult.isFailure) {
|
if (invoiceResult.isFailure) {
|
||||||
val e = invoiceResult.exceptionOrNull() ?: Exception("Failed to create invoice")
|
val e = invoiceResult.exceptionOrNull()
|
||||||
|
?: Exception(failedToCreateInvoice)
|
||||||
Log.e(TAG, "LNURL-WITHDRAW [INVOICE ERROR] ${e.message}")
|
Log.e(TAG, "LNURL-WITHDRAW [INVOICE ERROR] ${e.message}")
|
||||||
_receiveState.value = ReceiveState.Error(parseApiError(e, "Failed to create invoice"))
|
_receiveState.value = ReceiveState.Error(
|
||||||
|
parseApiError(e, failedToCreateInvoice) // ← localised
|
||||||
|
)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
val invoice = invoiceResult.getOrThrow()
|
val invoice = invoiceResult.getOrThrow()
|
||||||
@@ -122,14 +146,16 @@ class ReceiveViewModel(
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
val reason = response.reason?.ifBlank { null }
|
val reason = response.reason?.ifBlank { null }
|
||||||
?: "Withdraw service rejected the invoice"
|
?: withdrawRejected // ← localised
|
||||||
Log.w(TAG, "LNURL-WITHDRAW [CALLBACK ERR] $reason")
|
Log.w(TAG, "LNURL-WITHDRAW [CALLBACK ERR] $reason")
|
||||||
_receiveState.value = ReceiveState.Error(reason)
|
_receiveState.value = ReceiveState.Error(reason)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onFailure { e ->
|
.onFailure { e ->
|
||||||
Log.w(TAG, "LNURL-WITHDRAW [CALLBACK FAIL] ${e.message}")
|
Log.w(TAG, "LNURL-WITHDRAW [CALLBACK FAIL] ${e.message}")
|
||||||
_receiveState.value = ReceiveState.Error(parseApiError(e, "Withdraw failed"))
|
_receiveState.value = ReceiveState.Error(
|
||||||
|
parseApiError(e, withdrawFailed) // ← localised
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user