555 lines
26 KiB
Kotlin
555 lines
26 KiB
Kotlin
package com.bitcointxoko.gudariwallet.ui
|
|
|
|
import androidx.compose.foundation.layout.*
|
|
import androidx.compose.foundation.rememberScrollState
|
|
import androidx.compose.foundation.verticalScroll
|
|
import androidx.compose.foundation.combinedClickable
|
|
import androidx.compose.material3.*
|
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
|
import androidx.compose.runtime.*
|
|
import androidx.compose.ui.Alignment
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.unit.dp
|
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
import androidx.compose.foundation.clickable
|
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
import androidx.compose.material.icons.Icons
|
|
import androidx.compose.material.icons.filled.Visibility
|
|
import androidx.compose.material.icons.filled.VisibilityOff
|
|
import androidx.compose.foundation.lazy.LazyColumn
|
|
import androidx.compose.foundation.lazy.items
|
|
import androidx.compose.material.icons.filled.ArrowDropDown
|
|
import androidx.compose.material.icons.filled.Check
|
|
import androidx.compose.material.icons.filled.Search
|
|
import androidx.compose.material.icons.filled.Language // ← NEW
|
|
import androidx.compose.ui.text.input.ImeAction
|
|
import androidx.compose.foundation.text.KeyboardActions
|
|
import androidx.compose.foundation.text.KeyboardOptions
|
|
import androidx.compose.material.icons.filled.Cable
|
|
import androidx.compose.material.icons.filled.People
|
|
import androidx.compose.ui.text.input.KeyboardType
|
|
import androidx.compose.ui.platform.LocalFocusManager
|
|
import com.bitcointxoko.gudariwallet.util.formatFiat
|
|
import androidx.compose.ui.text.font.FontFamily
|
|
import com.bitcointxoko.gudariwallet.LocalAppStrings
|
|
import cafe.adriel.lyricist.Lyricist // ← NEW
|
|
import com.bitcointxoko.gudariwallet.i18n.AppStrings
|
|
|
|
@OptIn(ExperimentalMaterial3Api::class)
|
|
@Composable
|
|
fun HomeTab(
|
|
vm: WalletViewModel,
|
|
onHistoryClick: () -> Unit,
|
|
onNwcClick : () -> Unit,
|
|
onContactsClick: () -> Unit,
|
|
lyricist: Lyricist<AppStrings> // ← NEW: passed in from wherever you call ProvideStrings
|
|
) {
|
|
val strings = LocalAppStrings.current
|
|
|
|
val balanceState by vm.balanceState.collectAsState()
|
|
val balanceHidden by vm.balanceHidden.collectAsState()
|
|
|
|
// ── Language switcher state ──────────────────────────────────────────
|
|
// val supportedTags = remember { listOf("en", "es", "eu") } // add more as you add locale files
|
|
|
|
val isRefreshing = balanceState is BalanceState.Loading
|
|
|| (balanceState as? BalanceState.Success)?.isRefreshing == true
|
|
Box(modifier = Modifier.fillMaxSize()) {
|
|
PullToRefreshBox(
|
|
isRefreshing = isRefreshing,
|
|
onRefresh = { vm.refreshBalance() },
|
|
modifier = Modifier.fillMaxSize()
|
|
) {
|
|
Column(
|
|
modifier = Modifier
|
|
.fillMaxSize()
|
|
.verticalScroll(rememberScrollState())
|
|
.padding(24.dp),
|
|
horizontalAlignment = Alignment.CenterHorizontally,
|
|
verticalArrangement = Arrangement.Center
|
|
) {
|
|
when (val s = balanceState) {
|
|
|
|
is BalanceState.Loading -> {
|
|
CircularProgressIndicator()
|
|
Spacer(Modifier.height(12.dp))
|
|
Text(
|
|
text = strings.loadingBalance,
|
|
style = MaterialTheme.typography.bodyMedium,
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
)
|
|
}
|
|
|
|
is BalanceState.Success -> {
|
|
val selectedCurrency by vm.selectedCurrency.collectAsState()
|
|
var showCurrencyPicker by remember { mutableStateOf(false) }
|
|
var isLoadingCurrencies by remember { mutableStateOf(false) }
|
|
var currencyList by remember { mutableStateOf<List<String>>(emptyList()) }
|
|
LaunchedEffect(showCurrencyPicker) {
|
|
if (showCurrencyPicker && currencyList.isEmpty()) {
|
|
isLoadingCurrencies = true
|
|
currencyList = vm.getSupportedCurrencies()
|
|
isLoadingCurrencies = false
|
|
}
|
|
}
|
|
Row(
|
|
verticalAlignment = Alignment.CenterVertically
|
|
) {
|
|
Icon(
|
|
imageVector = if (balanceHidden)
|
|
Icons.Filled.VisibilityOff
|
|
else
|
|
Icons.Filled.Visibility,
|
|
contentDescription = null,
|
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
modifier = Modifier.size(20.dp)
|
|
)
|
|
|
|
Spacer(Modifier.width(8.dp))
|
|
|
|
Text(
|
|
text = if (balanceHidden) "••••••" else formatSats(s.sats),
|
|
style = MaterialTheme.typography.displayLarge.copy(
|
|
fontFamily = FontFamily.Monospace
|
|
),
|
|
color = if (balanceHidden)
|
|
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.35f)
|
|
else
|
|
MaterialTheme.colorScheme.primary,
|
|
maxLines = 1,
|
|
softWrap = false,
|
|
modifier = Modifier
|
|
.widthIn(max = 280.dp)
|
|
.combinedClickable(
|
|
onClick = { if (!balanceHidden) vm.toggleBalanceVisibility() },
|
|
onClickLabel = if (balanceHidden) null else strings.hideBalance,
|
|
onLongClick = { if (balanceHidden) vm.toggleBalanceVisibility() },
|
|
onLongClickLabel = if (balanceHidden) strings.revealBalance else null,
|
|
indication = null,
|
|
interactionSource = remember { MutableInteractionSource() }
|
|
)
|
|
)
|
|
Spacer(Modifier.width(8.dp))
|
|
Text(
|
|
text = strings.sats,
|
|
style = MaterialTheme.typography.titleMedium.copy(
|
|
fontFamily = FontFamily.Monospace
|
|
),
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
)
|
|
Spacer(Modifier.width(8.dp))
|
|
}
|
|
Spacer(Modifier.height(2.dp))
|
|
val currency = selectedCurrency
|
|
if (currency != null) {
|
|
Row(
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
horizontalArrangement = Arrangement.Center
|
|
) {
|
|
if (s.fiatAmount != null && s.fiatCurrency != null) {
|
|
Text(
|
|
text = if (balanceHidden) "••••"
|
|
else formatFiat(s.fiatAmount, s.fiatCurrency),
|
|
style = MaterialTheme.typography.bodyMedium.copy(
|
|
fontFamily = FontFamily.Monospace
|
|
),
|
|
color = if (balanceHidden)
|
|
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.35f)
|
|
else
|
|
MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.75f),
|
|
maxLines = 1,
|
|
softWrap = false
|
|
)
|
|
Spacer(Modifier.width(4.dp))
|
|
}
|
|
Row(
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
modifier = Modifier
|
|
.clickable(
|
|
onClick = { showCurrencyPicker = true },
|
|
onClickLabel = strings.changeCurrency,
|
|
indication = null,
|
|
interactionSource = remember { MutableInteractionSource() }
|
|
)
|
|
.padding(horizontal = 4.dp, vertical = 4.dp)
|
|
) {
|
|
Text(
|
|
text = currency,
|
|
style = MaterialTheme.typography.bodyMedium.copy(
|
|
fontFamily = FontFamily.Monospace
|
|
),
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(
|
|
alpha = 0.75f
|
|
)
|
|
)
|
|
Spacer(Modifier.width(2.dp))
|
|
Icon(
|
|
imageVector = Icons.Filled.ArrowDropDown,
|
|
contentDescription = strings.changeCurrency,
|
|
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),
|
|
modifier = Modifier.size(14.dp)
|
|
)
|
|
}
|
|
}
|
|
} else {
|
|
TextButton(
|
|
onClick = { showCurrencyPicker = true },
|
|
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 2.dp)
|
|
) {
|
|
Text(
|
|
text = strings.showFiat,
|
|
style = MaterialTheme.typography.bodySmall,
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.45f)
|
|
)
|
|
}
|
|
}
|
|
|
|
Spacer(Modifier.height(8.dp))
|
|
|
|
if (!s.isRefreshing) {
|
|
val updatedLabel: String = remember(s.lastUpdated) {
|
|
formatLastUpdated(s.lastUpdated, strings)
|
|
}
|
|
Text(
|
|
text = strings.updatedAt(updatedLabel),
|
|
style = MaterialTheme.typography.bodySmall,
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
)
|
|
}
|
|
|
|
Spacer(Modifier.height(16.dp))
|
|
|
|
TextButton(onClick = onHistoryClick) {
|
|
Text(
|
|
text = strings.viewHistory,
|
|
style = MaterialTheme.typography.labelLarge,
|
|
color = MaterialTheme.colorScheme.primary
|
|
)
|
|
}
|
|
|
|
// ── NEW: Language switcher ─────────────────────────────────
|
|
// Spacer(Modifier.height(8.dp))
|
|
// LanguageSwitcherButton(
|
|
// currentTag = lyricist.languageTag,
|
|
// supportedTags = supportedTags,
|
|
// onSelect = { tag -> lyricist.languageTag = tag }
|
|
// )
|
|
// ──────────────────────────────────────────────────────────
|
|
|
|
if (showCurrencyPicker) {
|
|
CurrencyPickerSheet(
|
|
currencies = currencyList,
|
|
selectedCurrency = selectedCurrency,
|
|
onSelect = { code ->
|
|
vm.setSelectedCurrency(code)
|
|
showCurrencyPicker = false
|
|
},
|
|
isLoading = isLoadingCurrencies,
|
|
onDismiss = { showCurrencyPicker = false }
|
|
)
|
|
}
|
|
}
|
|
|
|
is BalanceState.Error -> {
|
|
if (s.staleSats != null) {
|
|
Text(
|
|
text = if (balanceHidden) "••••••" else formatSats(s.staleSats),
|
|
style = MaterialTheme.typography.displayLarge.copy(
|
|
fontFamily = FontFamily.Monospace
|
|
),
|
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f)
|
|
)
|
|
Text(
|
|
text = strings.sats,
|
|
style = MaterialTheme.typography.titleMedium,
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f)
|
|
)
|
|
Spacer(Modifier.height(12.dp))
|
|
}
|
|
|
|
Surface(
|
|
color = MaterialTheme.colorScheme.errorContainer,
|
|
shape = MaterialTheme.shapes.small,
|
|
modifier = Modifier.fillMaxWidth(0.85f)
|
|
) {
|
|
Text(
|
|
text = if (s.staleSats != null)
|
|
strings.couldNotRefresh
|
|
else
|
|
strings.couldNotLoadBalance,
|
|
style = MaterialTheme.typography.bodySmall,
|
|
color = MaterialTheme.colorScheme.onErrorContainer,
|
|
modifier = Modifier.padding(12.dp)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Row(
|
|
modifier = Modifier
|
|
.align(Alignment.TopEnd)
|
|
.padding(8.dp)
|
|
) {
|
|
IconButton(onClick = onContactsClick) {
|
|
Icon(
|
|
imageVector = Icons.Filled.People,
|
|
contentDescription = "Contacts",
|
|
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
|
|
)
|
|
}
|
|
IconButton(onClick = onNwcClick) {
|
|
Icon(
|
|
imageVector = Icons.Filled.Cable,
|
|
contentDescription = "Wallet Connect",
|
|
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── NEW: Language switcher composable ─────────────────────────────────────────
|
|
@Composable
|
|
private fun LanguageSwitcherButton(
|
|
currentTag : String,
|
|
supportedTags: List<String>,
|
|
onSelect : (String) -> Unit
|
|
) {
|
|
var expanded by remember { mutableStateOf(false) }
|
|
|
|
// Human-readable display names for each tag
|
|
val displayName: (String) -> String = { tag ->
|
|
when (tag) {
|
|
"en" -> "English"
|
|
"es" -> "Español"
|
|
"eu" -> "Euskara"
|
|
else -> tag.uppercase()
|
|
}
|
|
}
|
|
|
|
Box {
|
|
TextButton(
|
|
onClick = { expanded = true },
|
|
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 2.dp)
|
|
) {
|
|
Icon(
|
|
imageVector = Icons.Filled.Language,
|
|
contentDescription = null,
|
|
modifier = Modifier.size(16.dp),
|
|
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
|
)
|
|
Spacer(Modifier.width(4.dp))
|
|
Text(
|
|
text = displayName(currentTag),
|
|
style = MaterialTheme.typography.bodySmall,
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
|
)
|
|
Icon(
|
|
imageVector = Icons.Filled.ArrowDropDown,
|
|
contentDescription = null,
|
|
modifier = Modifier.size(14.dp),
|
|
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f)
|
|
)
|
|
}
|
|
|
|
DropdownMenu(
|
|
expanded = expanded,
|
|
onDismissRequest = { expanded = false }
|
|
) {
|
|
supportedTags.forEach { tag ->
|
|
DropdownMenuItem(
|
|
text = {
|
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
|
Text(
|
|
text = displayName(tag),
|
|
style = MaterialTheme.typography.bodyMedium,
|
|
color = if (tag == currentTag)
|
|
MaterialTheme.colorScheme.primary
|
|
else
|
|
MaterialTheme.colorScheme.onSurface
|
|
)
|
|
}
|
|
},
|
|
trailingIcon = if (tag == currentTag) ({
|
|
Icon(
|
|
imageVector = Icons.Filled.Check,
|
|
contentDescription = null,
|
|
tint = MaterialTheme.colorScheme.primary,
|
|
modifier = Modifier.size(16.dp)
|
|
)
|
|
}) else null,
|
|
onClick = {
|
|
onSelect(tag)
|
|
expanded = false
|
|
}
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
|
|
private fun formatSats(sats: Long): String =
|
|
"%,d".format(sats).replace(',', ' ')
|
|
|
|
private fun formatLastUpdated(epochMs: Long, strings: AppStrings): String {
|
|
val ageMs = System.currentTimeMillis() - epochMs
|
|
return when {
|
|
ageMs < 60_000L -> strings.justNow
|
|
ageMs < 3_600_000L -> strings.minAgo((ageMs / 60_000L).toInt())
|
|
else -> {
|
|
val formatter = java.time.format.DateTimeFormatter
|
|
.ofPattern("HH:mm")
|
|
.withZone(java.time.ZoneId.systemDefault())
|
|
strings.atTime(formatter.format(java.time.Instant.ofEpochMilli(epochMs)))
|
|
}
|
|
}
|
|
}
|
|
|
|
@OptIn(ExperimentalMaterial3Api::class)
|
|
@Composable
|
|
private fun CurrencyPickerSheet(
|
|
currencies : List<String>,
|
|
isLoading : Boolean,
|
|
selectedCurrency: String?,
|
|
onSelect : (String?) -> Unit,
|
|
onDismiss : () -> Unit
|
|
) {
|
|
val strings = LocalAppStrings.current
|
|
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
|
var query by remember { mutableStateOf("") }
|
|
val focusManager = LocalFocusManager.current
|
|
|
|
val filtered = remember(query, currencies) {
|
|
if (query.isBlank()) currencies
|
|
else currencies.filter { it.contains(query.trim(), ignoreCase = true) }
|
|
}
|
|
|
|
ModalBottomSheet(
|
|
onDismissRequest = onDismiss,
|
|
sheetState = sheetState
|
|
) {
|
|
Column(modifier = Modifier.fillMaxWidth()) {
|
|
|
|
Text(
|
|
text = strings.selectCurrency,
|
|
style = MaterialTheme.typography.titleMedium,
|
|
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp)
|
|
)
|
|
|
|
ListItem(
|
|
headlineContent = {
|
|
Text(
|
|
text = strings.currencyNone,
|
|
style = MaterialTheme.typography.bodyLarge,
|
|
color = if (selectedCurrency == null)
|
|
MaterialTheme.colorScheme.primary
|
|
else
|
|
MaterialTheme.colorScheme.onSurface
|
|
)
|
|
},
|
|
supportingContent = {
|
|
Text(
|
|
text = strings.hideFiatEquivalent,
|
|
style = MaterialTheme.typography.bodySmall,
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
)
|
|
},
|
|
trailingContent = if (selectedCurrency == null) ({
|
|
Icon(
|
|
imageVector = Icons.Filled.Check,
|
|
contentDescription = strings.selected,
|
|
tint = MaterialTheme.colorScheme.primary,
|
|
modifier = Modifier.size(18.dp)
|
|
)
|
|
}) else null,
|
|
modifier = Modifier.clickable { onSelect(null) }
|
|
)
|
|
HorizontalDivider(thickness = 1.dp)
|
|
|
|
if (!isLoading) {
|
|
OutlinedTextField(
|
|
value = query,
|
|
onValueChange = { query = it },
|
|
placeholder = { Text(strings.search) },
|
|
leadingIcon = {
|
|
Icon(
|
|
imageVector = Icons.Filled.Search,
|
|
contentDescription = null,
|
|
modifier = Modifier.size(18.dp)
|
|
)
|
|
},
|
|
singleLine = true,
|
|
keyboardOptions = KeyboardOptions(
|
|
keyboardType = KeyboardType.Text,
|
|
imeAction = ImeAction.Search
|
|
),
|
|
keyboardActions = KeyboardActions(
|
|
onSearch = { focusManager.clearFocus() }
|
|
),
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
|
)
|
|
}
|
|
|
|
when {
|
|
isLoading -> {
|
|
Box(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.padding(vertical = 48.dp),
|
|
contentAlignment = Alignment.Center
|
|
) {
|
|
CircularProgressIndicator(modifier = Modifier.size(28.dp))
|
|
}
|
|
}
|
|
|
|
filtered.isEmpty() -> {
|
|
Text(
|
|
text = if (query.isBlank()) strings.noCurrenciesAvailable
|
|
else strings.noCurrenciesMatch(query),
|
|
style = MaterialTheme.typography.bodySmall,
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp)
|
|
)
|
|
}
|
|
|
|
else -> {
|
|
LazyColumn(
|
|
modifier = Modifier.fillMaxWidth(),
|
|
contentPadding = PaddingValues(bottom = 32.dp)
|
|
) {
|
|
items(filtered, key = { it }) { code ->
|
|
val isSelected = code == selectedCurrency
|
|
ListItem(
|
|
headlineContent = {
|
|
Text(
|
|
text = code,
|
|
style = MaterialTheme.typography.bodyLarge,
|
|
color = if (isSelected)
|
|
MaterialTheme.colorScheme.primary
|
|
else
|
|
MaterialTheme.colorScheme.onSurface
|
|
)
|
|
},
|
|
trailingContent = if (isSelected) ({
|
|
Icon(
|
|
imageVector = Icons.Filled.Check,
|
|
contentDescription = strings.selected,
|
|
tint = MaterialTheme.colorScheme.primary,
|
|
modifier = Modifier.size(18.dp)
|
|
)
|
|
}) else null,
|
|
modifier = Modifier.clickable { onSelect(code) }
|
|
)
|
|
HorizontalDivider(thickness = 0.5.dp)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|