feat: improve swap price chart — real data only, 24h stats, min/max
This commit is contained in:
+4
-1
@@ -573,5 +573,8 @@
|
||||
"swapSuccess": "Swap abgeschlossen",
|
||||
"swapErrorInsufficient": "Unzureichendes Guthaben",
|
||||
"swapErrorExpired": "Angebot abgelaufen",
|
||||
"swapErrorGeneric": "Swap-Fehler: {error}"
|
||||
"swapErrorGeneric": "Swap-Fehler: {error}",
|
||||
"swapChartUnavailable": "Preis nicht verfügbar · Tippen zum Wiederholen",
|
||||
"swapChartMin": "Min",
|
||||
"swapChartMax": "Max"
|
||||
}
|
||||
|
||||
+4
-1
@@ -466,5 +466,8 @@
|
||||
"swapSuccess": "Swap completed",
|
||||
"swapErrorInsufficient": "Insufficient balance",
|
||||
"swapErrorExpired": "Quote has expired",
|
||||
"swapErrorGeneric": "Swap error: {error}"
|
||||
"swapErrorGeneric": "Swap error: {error}",
|
||||
"swapChartUnavailable": "Price unavailable · Tap to retry",
|
||||
"swapChartMin": "Min",
|
||||
"swapChartMax": "Max"
|
||||
}
|
||||
|
||||
+4
-1
@@ -596,5 +596,8 @@
|
||||
"placeholders": {
|
||||
"error": { "type": "String" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"swapChartUnavailable": "Precio no disponible · Toca para reintentar",
|
||||
"swapChartMin": "Mín",
|
||||
"swapChartMax": "Máx"
|
||||
}
|
||||
|
||||
+4
-1
@@ -573,5 +573,8 @@
|
||||
"swapSuccess": "Swap terminé",
|
||||
"swapErrorInsufficient": "Solde insuffisant",
|
||||
"swapErrorExpired": "Le devis a expiré",
|
||||
"swapErrorGeneric": "Erreur de swap : {error}"
|
||||
"swapErrorGeneric": "Erreur de swap : {error}",
|
||||
"swapChartUnavailable": "Prix indisponible · Appuyez pour réessayer",
|
||||
"swapChartMin": "Min",
|
||||
"swapChartMax": "Max"
|
||||
}
|
||||
|
||||
+4
-1
@@ -573,5 +573,8 @@
|
||||
"swapSuccess": "Swap completato",
|
||||
"swapErrorInsufficient": "Saldo insufficiente",
|
||||
"swapErrorExpired": "Il preventivo è scaduto",
|
||||
"swapErrorGeneric": "Errore swap: {error}"
|
||||
"swapErrorGeneric": "Errore swap: {error}",
|
||||
"swapChartUnavailable": "Prezzo non disponibile · Tocca per riprovare",
|
||||
"swapChartMin": "Min",
|
||||
"swapChartMax": "Max"
|
||||
}
|
||||
|
||||
+4
-1
@@ -573,5 +573,8 @@
|
||||
"swapSuccess": "スワップ完了",
|
||||
"swapErrorInsufficient": "残高不足",
|
||||
"swapErrorExpired": "見積もりの有効期限切れ",
|
||||
"swapErrorGeneric": "スワップエラー: {error}"
|
||||
"swapErrorGeneric": "スワップエラー: {error}",
|
||||
"swapChartUnavailable": "価格を取得できません・タップで再試行",
|
||||
"swapChartMin": "安値",
|
||||
"swapChartMax": "高値"
|
||||
}
|
||||
|
||||
+4
-1
@@ -573,5 +573,8 @@
|
||||
"swapSuccess": "교환 완료",
|
||||
"swapErrorInsufficient": "잔액 부족",
|
||||
"swapErrorExpired": "견적이 만료되었습니다",
|
||||
"swapErrorGeneric": "교환 오류: {error}"
|
||||
"swapErrorGeneric": "교환 오류: {error}",
|
||||
"swapChartUnavailable": "가격 불러오기 실패 · 탭하여 재시도",
|
||||
"swapChartMin": "최저",
|
||||
"swapChartMax": "최고"
|
||||
}
|
||||
|
||||
@@ -2430,6 +2430,24 @@ abstract class L10n {
|
||||
/// In es, this message translates to:
|
||||
/// **'Error en el swap: {error}'**
|
||||
String swapErrorGeneric(String error);
|
||||
|
||||
/// No description provided for @swapChartUnavailable.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Precio no disponible · Toca para reintentar'**
|
||||
String get swapChartUnavailable;
|
||||
|
||||
/// No description provided for @swapChartMin.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Mín'**
|
||||
String get swapChartMin;
|
||||
|
||||
/// No description provided for @swapChartMax.
|
||||
///
|
||||
/// In es, this message translates to:
|
||||
/// **'Máx'**
|
||||
String get swapChartMax;
|
||||
}
|
||||
|
||||
class _L10nDelegate extends LocalizationsDelegate<L10n> {
|
||||
|
||||
@@ -1284,4 +1284,14 @@ class L10nDe extends L10n {
|
||||
String swapErrorGeneric(String error) {
|
||||
return 'Swap-Fehler: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get swapChartUnavailable =>
|
||||
'Preis nicht verfügbar · Tippen zum Wiederholen';
|
||||
|
||||
@override
|
||||
String get swapChartMin => 'Min';
|
||||
|
||||
@override
|
||||
String get swapChartMax => 'Max';
|
||||
}
|
||||
|
||||
@@ -1265,4 +1265,13 @@ class L10nEn extends L10n {
|
||||
String swapErrorGeneric(String error) {
|
||||
return 'Swap error: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get swapChartUnavailable => 'Price unavailable · Tap to retry';
|
||||
|
||||
@override
|
||||
String get swapChartMin => 'Min';
|
||||
|
||||
@override
|
||||
String get swapChartMax => 'Max';
|
||||
}
|
||||
|
||||
@@ -1273,4 +1273,14 @@ class L10nEs extends L10n {
|
||||
String swapErrorGeneric(String error) {
|
||||
return 'Error en el swap: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get swapChartUnavailable =>
|
||||
'Precio no disponible · Toca para reintentar';
|
||||
|
||||
@override
|
||||
String get swapChartMin => 'Mín';
|
||||
|
||||
@override
|
||||
String get swapChartMax => 'Máx';
|
||||
}
|
||||
|
||||
@@ -1289,4 +1289,14 @@ class L10nFr extends L10n {
|
||||
String swapErrorGeneric(String error) {
|
||||
return 'Erreur de swap : $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get swapChartUnavailable =>
|
||||
'Prix indisponible · Appuyez pour réessayer';
|
||||
|
||||
@override
|
||||
String get swapChartMin => 'Min';
|
||||
|
||||
@override
|
||||
String get swapChartMax => 'Max';
|
||||
}
|
||||
|
||||
@@ -1277,4 +1277,14 @@ class L10nIt extends L10n {
|
||||
String swapErrorGeneric(String error) {
|
||||
return 'Errore swap: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get swapChartUnavailable =>
|
||||
'Prezzo non disponibile · Tocca per riprovare';
|
||||
|
||||
@override
|
||||
String get swapChartMin => 'Min';
|
||||
|
||||
@override
|
||||
String get swapChartMax => 'Max';
|
||||
}
|
||||
|
||||
@@ -1250,4 +1250,13 @@ class L10nJa extends L10n {
|
||||
String swapErrorGeneric(String error) {
|
||||
return 'スワップエラー: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get swapChartUnavailable => '価格を取得できません・タップで再試行';
|
||||
|
||||
@override
|
||||
String get swapChartMin => '安値';
|
||||
|
||||
@override
|
||||
String get swapChartMax => '高値';
|
||||
}
|
||||
|
||||
@@ -1252,4 +1252,13 @@ class L10nKo extends L10n {
|
||||
String swapErrorGeneric(String error) {
|
||||
return '교환 오류: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get swapChartUnavailable => '가격 불러오기 실패 · 탭하여 재시도';
|
||||
|
||||
@override
|
||||
String get swapChartMin => '최저';
|
||||
|
||||
@override
|
||||
String get swapChartMax => '최고';
|
||||
}
|
||||
|
||||
@@ -1277,4 +1277,14 @@ class L10nPt extends L10n {
|
||||
String swapErrorGeneric(String error) {
|
||||
return 'Erro no swap: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get swapChartUnavailable =>
|
||||
'Preço indisponível · Toque para tentar novamente';
|
||||
|
||||
@override
|
||||
String get swapChartMin => 'Mín';
|
||||
|
||||
@override
|
||||
String get swapChartMax => 'Máx';
|
||||
}
|
||||
|
||||
@@ -1273,4 +1273,13 @@ class L10nRu extends L10n {
|
||||
String swapErrorGeneric(String error) {
|
||||
return 'Ошибка обмена: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get swapChartUnavailable => 'Цена недоступна · Нажмите для повтора';
|
||||
|
||||
@override
|
||||
String get swapChartMin => 'Мин';
|
||||
|
||||
@override
|
||||
String get swapChartMax => 'Макс';
|
||||
}
|
||||
|
||||
@@ -1274,4 +1274,13 @@ class L10nSw extends L10n {
|
||||
String swapErrorGeneric(String error) {
|
||||
return 'Kosa la ubadilishaji: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get swapChartUnavailable => 'Bei haipatikani · Gusa kurudia';
|
||||
|
||||
@override
|
||||
String get swapChartMin => 'Chini';
|
||||
|
||||
@override
|
||||
String get swapChartMax => 'Juu';
|
||||
}
|
||||
|
||||
@@ -1245,4 +1245,13 @@ class L10nZh extends L10n {
|
||||
String swapErrorGeneric(String error) {
|
||||
return '兑换错误: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get swapChartUnavailable => '价格不可用 · 点击重试';
|
||||
|
||||
@override
|
||||
String get swapChartMin => '最低';
|
||||
|
||||
@override
|
||||
String get swapChartMax => '最高';
|
||||
}
|
||||
|
||||
+4
-1
@@ -573,5 +573,8 @@
|
||||
"swapSuccess": "Swap concluído",
|
||||
"swapErrorInsufficient": "Saldo insuficiente",
|
||||
"swapErrorExpired": "A cotação expirou",
|
||||
"swapErrorGeneric": "Erro no swap: {error}"
|
||||
"swapErrorGeneric": "Erro no swap: {error}",
|
||||
"swapChartUnavailable": "Preço indisponível · Toque para tentar novamente",
|
||||
"swapChartMin": "Mín",
|
||||
"swapChartMax": "Máx"
|
||||
}
|
||||
|
||||
+4
-1
@@ -573,5 +573,8 @@
|
||||
"swapSuccess": "Обмен завершён",
|
||||
"swapErrorInsufficient": "Недостаточный баланс",
|
||||
"swapErrorExpired": "Котировка истекла",
|
||||
"swapErrorGeneric": "Ошибка обмена: {error}"
|
||||
"swapErrorGeneric": "Ошибка обмена: {error}",
|
||||
"swapChartUnavailable": "Цена недоступна · Нажмите для повтора",
|
||||
"swapChartMin": "Мин",
|
||||
"swapChartMax": "Макс"
|
||||
}
|
||||
|
||||
+4
-1
@@ -573,5 +573,8 @@
|
||||
"swapSuccess": "Ubadilishaji umekamilika",
|
||||
"swapErrorInsufficient": "Salio haitoshi",
|
||||
"swapErrorExpired": "Bei imeisha muda",
|
||||
"swapErrorGeneric": "Kosa la ubadilishaji: {error}"
|
||||
"swapErrorGeneric": "Kosa la ubadilishaji: {error}",
|
||||
"swapChartUnavailable": "Bei haipatikani · Gusa kurudia",
|
||||
"swapChartMin": "Chini",
|
||||
"swapChartMax": "Juu"
|
||||
}
|
||||
|
||||
+4
-1
@@ -573,5 +573,8 @@
|
||||
"swapSuccess": "兑换完成",
|
||||
"swapErrorInsufficient": "余额不足",
|
||||
"swapErrorExpired": "报价已过期",
|
||||
"swapErrorGeneric": "兑换错误: {error}"
|
||||
"swapErrorGeneric": "兑换错误: {error}",
|
||||
"swapChartUnavailable": "价格不可用 · 点击重试",
|
||||
"swapChartMin": "最低",
|
||||
"swapChartMax": "最高"
|
||||
}
|
||||
|
||||
@@ -44,6 +44,7 @@ class _SwapScreenState extends State<SwapScreen>
|
||||
|
||||
List<double> _chartData = [];
|
||||
bool _isLoadingChart = true;
|
||||
bool _chartError = false;
|
||||
|
||||
// --- Swap state ---
|
||||
bool _isSwapping = false;
|
||||
@@ -81,7 +82,6 @@ class _SwapScreenState extends State<SwapScreen>
|
||||
Future<void> _loadBalances() async {
|
||||
final walletProvider = context.read<WalletProvider>();
|
||||
final mintUrl = WalletProvider.cubaBitcoinMint;
|
||||
if (mintUrl == null) return;
|
||||
|
||||
try {
|
||||
final balances = await walletProvider.getBalancesForMint(mintUrl);
|
||||
@@ -96,6 +96,10 @@ class _SwapScreenState extends State<SwapScreen>
|
||||
}
|
||||
|
||||
Future<void> _loadChartData() async {
|
||||
setState(() {
|
||||
_isLoadingChart = true;
|
||||
_chartError = false;
|
||||
});
|
||||
try {
|
||||
final prices = await PriceService.getHistoricalPrices(range: 'ONE_DAY');
|
||||
if (mounted && prices.isNotEmpty) {
|
||||
@@ -107,22 +111,14 @@ class _SwapScreenState extends State<SwapScreen>
|
||||
} catch (_) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_chartData = _generateMockData();
|
||||
_chartData = [];
|
||||
_isLoadingChart = false;
|
||||
_chartError = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<double> _generateMockData() {
|
||||
final priceProvider = context.read<PriceProvider>();
|
||||
final basePrice = priceProvider.btcPriceUsd ?? 65000;
|
||||
final rng = Random();
|
||||
return List.generate(24, (i) {
|
||||
return basePrice + (rng.nextDouble() - 0.48) * basePrice * 0.02;
|
||||
});
|
||||
}
|
||||
|
||||
void _onFromChanged() {
|
||||
if (_isUpdating) return;
|
||||
_lastEditedFrom = true;
|
||||
@@ -263,7 +259,6 @@ class _SwapScreenState extends State<SwapScreen>
|
||||
Future<void> _startSwap() async {
|
||||
final walletProvider = context.read<WalletProvider>();
|
||||
final mintUrl = WalletProvider.cubaBitcoinMint;
|
||||
if (mintUrl == null) return;
|
||||
|
||||
final destAmount = _getDestAmount();
|
||||
if (destAmount <= BigInt.zero) return;
|
||||
@@ -708,24 +703,42 @@ class _SwapScreenState extends State<SwapScreen>
|
||||
// --- Widgets ---
|
||||
|
||||
Widget _buildPriceChart(PriceProvider priceProvider) {
|
||||
final l10n = L10n.of(context)!;
|
||||
final btcPrice = priceProvider.btcPriceUsd;
|
||||
final priceStr = btcPrice != null
|
||||
? '\$${NumberFormat('#,###').format(btcPrice.round())}'
|
||||
: '...';
|
||||
final fmt = NumberFormat('#,###');
|
||||
final priceStr = btcPrice != null ? '\$${fmt.format(btcPrice.round())}' : '...';
|
||||
|
||||
String rateStr = '...';
|
||||
if (btcPrice != null && btcPrice > 0) {
|
||||
final satsPerDollar = (100000000 / btcPrice).round();
|
||||
rateStr = '1 USD ≈ ${NumberFormat('#,###').format(satsPerDollar)} sats';
|
||||
rateStr = '1 USD ≈ ${fmt.format(satsPerDollar)} sats';
|
||||
}
|
||||
|
||||
final isUpTrend =
|
||||
_chartData.length >= 2 && _chartData.last >= _chartData.first;
|
||||
final hasData = _chartData.length >= 2;
|
||||
final isUpTrend = hasData && _chartData.last >= _chartData.first;
|
||||
final trendColor = isUpTrend ? AppColors.success : AppColors.error;
|
||||
|
||||
// % cambio 24h
|
||||
String? changeStr;
|
||||
if (hasData) {
|
||||
final change = ((_chartData.last - _chartData.first) / _chartData.first) * 100;
|
||||
final sign = change >= 0 ? '+' : '';
|
||||
changeStr = '$sign${change.toStringAsFixed(1)}% 24h';
|
||||
}
|
||||
|
||||
// Min/Max 24h
|
||||
String? minMaxStr;
|
||||
if (hasData) {
|
||||
final minVal = _chartData.reduce(min);
|
||||
final maxVal = _chartData.reduce(max);
|
||||
final minLabel = l10n.swapChartMin;
|
||||
final maxLabel = l10n.swapChartMax;
|
||||
minMaxStr = '24h $minLabel: \$${fmt.format(minVal.round())} — $maxLabel: \$${fmt.format(maxVal.round())}';
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
// Price
|
||||
// Precio centrado
|
||||
Text(
|
||||
priceStr,
|
||||
style: const TextStyle(
|
||||
@@ -736,18 +749,35 @@ class _SwapScreenState extends State<SwapScreen>
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
'USD',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.6),
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'BTC/USD',
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
if (changeStr != null) ...[
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
changeStr,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: trendColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Sparkline (sin contenedor, integrado)
|
||||
// Sparkline o estado de error
|
||||
SizedBox(
|
||||
height: 40,
|
||||
width: double.infinity,
|
||||
@@ -762,16 +792,45 @@ class _SwapScreenState extends State<SwapScreen>
|
||||
),
|
||||
),
|
||||
)
|
||||
: CustomPaint(
|
||||
painter: _SparklinePainter(
|
||||
data: _chartData,
|
||||
lineColor: trendColor,
|
||||
fillColor: trendColor.withValues(alpha: 0.08),
|
||||
),
|
||||
),
|
||||
: _chartError
|
||||
? GestureDetector(
|
||||
onTap: _loadChartData,
|
||||
child: Center(
|
||||
child: Text(
|
||||
l10n.swapChartUnavailable,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: CustomPaint(
|
||||
painter: _SparklinePainter(
|
||||
data: _chartData,
|
||||
lineColor: trendColor,
|
||||
fillColor: trendColor.withValues(alpha: 0.08),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Min/Max 24h
|
||||
if (minMaxStr != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4),
|
||||
child: Text(
|
||||
minMaxStr,
|
||||
style: TextStyle(
|
||||
fontFamily: 'Inter',
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.textSecondary.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Exchange rate
|
||||
Text(
|
||||
rateStr,
|
||||
|
||||
Reference in New Issue
Block a user