Compare commits

..
26 changed files with 724 additions and 64 deletions
+9 -1
View File
@@ -573,5 +573,13 @@
"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",
"swapChartMinMax": "24h Min: {minPrice} — Max: {maxPrice}",
"privacyPolicy": "Datenschutz",
"privacyTitle": "WIR SAMMELN NICHTS",
"privacyGoodbye": "TSCHÜSS",
"privacyKeepReading": "(lies weiter, wenn du willst…)",
"privacyBody": "Wir wissen nicht, wer du bist\nWir wissen nicht, wie viel du hast\nWir wissen nicht, was du tust",
"privacyConclusion": "Der beste Weg, deine Daten zu schützen,\nist sie nicht zu haben"
}
+9 -1
View File
@@ -466,5 +466,13 @@
"swapSuccess": "Swap completed",
"swapErrorInsufficient": "Insufficient balance",
"swapErrorExpired": "Quote has expired",
"swapErrorGeneric": "Swap error: {error}"
"swapErrorGeneric": "Swap error: {error}",
"swapChartUnavailable": "Price unavailable · Tap to retry",
"swapChartMinMax": "24h Min: {minPrice} — Max: {maxPrice}",
"privacyPolicy": "Privacy policy",
"privacyTitle": "WE COLLECT NOTHING",
"privacyGoodbye": "BYE",
"privacyKeepReading": "(keep reading if you want…)",
"privacyBody": "We don't know who you are\nWe don't know how much you have\nWe don't know what you do",
"privacyConclusion": "The best way to protect your data\nis not to have it"
}
+15 -1
View File
@@ -596,5 +596,19 @@
"placeholders": {
"error": { "type": "String" }
}
}
},
"swapChartUnavailable": "Precio no disponible · Toca para reintentar",
"swapChartMinMax": "24h Mín: {minPrice} — Máx: {maxPrice}",
"@swapChartMinMax": {
"placeholders": {
"minPrice": { "type": "String" },
"maxPrice": { "type": "String" }
}
},
"privacyPolicy": "Política de privacidad",
"privacyTitle": "NO RECOPILAMOS NADA",
"privacyGoodbye": "ADIÓS",
"privacyKeepReading": "(sigue leyendo si quieres…)",
"privacyBody": "No sabemos quién eres\nNo sabemos cuánto tienes\nNo sabemos qué haces",
"privacyConclusion": "La mejor forma de proteger tus datos\nes no tenerlos"
}
+9 -1
View File
@@ -573,5 +573,13 @@
"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",
"swapChartMinMax": "24h Min : {minPrice} — Max : {maxPrice}",
"privacyPolicy": "Politique de confidentialité",
"privacyTitle": "NOUS NE COLLECTONS RIEN",
"privacyGoodbye": "AU REVOIR",
"privacyKeepReading": "(continue à lire si tu veux…)",
"privacyBody": "Nous ne savons pas qui tu es\nNous ne savons pas combien tu as\nNous ne savons pas ce que tu fais",
"privacyConclusion": "La meilleure façon de protéger tes données,\nc'est de ne pas les avoir"
}
+9 -1
View File
@@ -573,5 +573,13 @@
"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",
"swapChartMinMax": "24h Min: {minPrice} — Max: {maxPrice}",
"privacyPolicy": "Informativa sulla privacy",
"privacyTitle": "NON RACCOGLIAMO NULLA",
"privacyGoodbye": "CIAO",
"privacyKeepReading": "(continua a leggere se vuoi…)",
"privacyBody": "Non sappiamo chi sei\nNon sappiamo quanto hai\nNon sappiamo cosa fai",
"privacyConclusion": "Il modo migliore per proteggere i tuoi dati\nè non averli"
}
+9 -1
View File
@@ -573,5 +573,13 @@
"swapSuccess": "スワップ完了",
"swapErrorInsufficient": "残高不足",
"swapErrorExpired": "見積もりの有効期限切れ",
"swapErrorGeneric": "スワップエラー: {error}"
"swapErrorGeneric": "スワップエラー: {error}",
"swapChartUnavailable": "価格を取得できません・タップで再試行",
"swapChartMinMax": "24h 安値: {minPrice} — 高値: {maxPrice}",
"privacyPolicy": "プライバシーポリシー",
"privacyTitle": "何も収集しません",
"privacyGoodbye": "さようなら",
"privacyKeepReading": "(読み続けたければどうぞ…)",
"privacyBody": "あなたが誰か知りません\nいくら持っているか知りません\n何をしているか知りません",
"privacyConclusion": "データを守る最善の方法は\nデータを持たないことです"
}
+9 -1
View File
@@ -573,5 +573,13 @@
"swapSuccess": "교환 완료",
"swapErrorInsufficient": "잔액 부족",
"swapErrorExpired": "견적이 만료되었습니다",
"swapErrorGeneric": "교환 오류: {error}"
"swapErrorGeneric": "교환 오류: {error}",
"swapChartUnavailable": "가격 불러오기 실패 · 탭하여 재시도",
"swapChartMinMax": "24h 최저: {minPrice} — 최고: {maxPrice}",
"privacyPolicy": "개인정보 처리방침",
"privacyTitle": "우리는 아무것도 수집하지 않습니다",
"privacyGoodbye": "안녕",
"privacyKeepReading": "(계속 읽고 싶으면…)",
"privacyBody": "당신이 누구인지 모릅니다\n얼마를 가지고 있는지 모릅니다\n무엇을 하는지 모릅니다",
"privacyConclusion": "데이터를 보호하는 가장 좋은 방법은\n데이터를 갖지 않는 것입니다"
}
+48
View File
@@ -2430,6 +2430,54 @@ 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 @swapChartMinMax.
///
/// In es, this message translates to:
/// **'24h Mín: {minPrice} — Máx: {maxPrice}'**
String swapChartMinMax(String minPrice, String maxPrice);
/// No description provided for @privacyPolicy.
///
/// In es, this message translates to:
/// **'Política de privacidad'**
String get privacyPolicy;
/// No description provided for @privacyTitle.
///
/// In es, this message translates to:
/// **'NO RECOPILAMOS NADA'**
String get privacyTitle;
/// No description provided for @privacyGoodbye.
///
/// In es, this message translates to:
/// **'ADIÓS'**
String get privacyGoodbye;
/// No description provided for @privacyKeepReading.
///
/// In es, this message translates to:
/// **'(sigue leyendo si quieres…)'**
String get privacyKeepReading;
/// No description provided for @privacyBody.
///
/// In es, this message translates to:
/// **'No sabemos quién eres\nNo sabemos cuánto tienes\nNo sabemos qué haces'**
String get privacyBody;
/// No description provided for @privacyConclusion.
///
/// In es, this message translates to:
/// **'La mejor forma de proteger tus datos\nes no tenerlos'**
String get privacyConclusion;
}
class _L10nDelegate extends LocalizationsDelegate<L10n> {
+29
View File
@@ -1284,4 +1284,33 @@ 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 swapChartMinMax(String minPrice, String maxPrice) {
return '24h Min: $minPrice — Max: $maxPrice';
}
@override
String get privacyPolicy => 'Datenschutz';
@override
String get privacyTitle => 'WIR SAMMELN NICHTS';
@override
String get privacyGoodbye => 'TSCHÜSS';
@override
String get privacyKeepReading => '(lies weiter, wenn du willst…)';
@override
String get privacyBody =>
'Wir wissen nicht, wer du bist\nWir wissen nicht, wie viel du hast\nWir wissen nicht, was du tust';
@override
String get privacyConclusion =>
'Der beste Weg, deine Daten zu schützen,\nist sie nicht zu haben';
}
+28
View File
@@ -1265,4 +1265,32 @@ class L10nEn extends L10n {
String swapErrorGeneric(String error) {
return 'Swap error: $error';
}
@override
String get swapChartUnavailable => 'Price unavailable · Tap to retry';
@override
String swapChartMinMax(String minPrice, String maxPrice) {
return '24h Min: $minPrice — Max: $maxPrice';
}
@override
String get privacyPolicy => 'Privacy policy';
@override
String get privacyTitle => 'WE COLLECT NOTHING';
@override
String get privacyGoodbye => 'BYE';
@override
String get privacyKeepReading => '(keep reading if you want…)';
@override
String get privacyBody =>
'We don\'t know who you are\nWe don\'t know how much you have\nWe don\'t know what you do';
@override
String get privacyConclusion =>
'The best way to protect your data\nis not to have it';
}
+29
View File
@@ -1273,4 +1273,33 @@ 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 swapChartMinMax(String minPrice, String maxPrice) {
return '24h Mín: $minPrice — Máx: $maxPrice';
}
@override
String get privacyPolicy => 'Política de privacidad';
@override
String get privacyTitle => 'NO RECOPILAMOS NADA';
@override
String get privacyGoodbye => 'ADIÓS';
@override
String get privacyKeepReading => '(sigue leyendo si quieres…)';
@override
String get privacyBody =>
'No sabemos quién eres\nNo sabemos cuánto tienes\nNo sabemos qué haces';
@override
String get privacyConclusion =>
'La mejor forma de proteger tus datos\nes no tenerlos';
}
+29
View File
@@ -1289,4 +1289,33 @@ 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 swapChartMinMax(String minPrice, String maxPrice) {
return '24h Min : $minPrice — Max : $maxPrice';
}
@override
String get privacyPolicy => 'Politique de confidentialité';
@override
String get privacyTitle => 'NOUS NE COLLECTONS RIEN';
@override
String get privacyGoodbye => 'AU REVOIR';
@override
String get privacyKeepReading => '(continue à lire si tu veux…)';
@override
String get privacyBody =>
'Nous ne savons pas qui tu es\nNous ne savons pas combien tu as\nNous ne savons pas ce que tu fais';
@override
String get privacyConclusion =>
'La meilleure façon de protéger tes données,\nc\'est de ne pas les avoir';
}
+29
View File
@@ -1277,4 +1277,33 @@ class L10nIt extends L10n {
String swapErrorGeneric(String error) {
return 'Errore swap: $error';
}
@override
String get swapChartUnavailable =>
'Prezzo non disponibile · Tocca per riprovare';
@override
String swapChartMinMax(String minPrice, String maxPrice) {
return '24h Min: $minPrice — Max: $maxPrice';
}
@override
String get privacyPolicy => 'Informativa sulla privacy';
@override
String get privacyTitle => 'NON RACCOGLIAMO NULLA';
@override
String get privacyGoodbye => 'CIAO';
@override
String get privacyKeepReading => '(continua a leggere se vuoi…)';
@override
String get privacyBody =>
'Non sappiamo chi sei\nNon sappiamo quanto hai\nNon sappiamo cosa fai';
@override
String get privacyConclusion =>
'Il modo migliore per proteggere i tuoi dati\nè non averli';
}
+26
View File
@@ -1250,4 +1250,30 @@ class L10nJa extends L10n {
String swapErrorGeneric(String error) {
return 'スワップエラー: $error';
}
@override
String get swapChartUnavailable => '価格を取得できません・タップで再試行';
@override
String swapChartMinMax(String minPrice, String maxPrice) {
return '24h 安値: $minPrice — 高値: $maxPrice';
}
@override
String get privacyPolicy => 'プライバシーポリシー';
@override
String get privacyTitle => '何も収集しません';
@override
String get privacyGoodbye => 'さようなら';
@override
String get privacyKeepReading => '(読み続けたければどうぞ…)';
@override
String get privacyBody => 'あなたが誰か知りません\nいくら持っているか知りません\n何をしているか知りません';
@override
String get privacyConclusion => 'データを守る最善の方法は\nデータを持たないことです';
}
+26
View File
@@ -1252,4 +1252,30 @@ class L10nKo extends L10n {
String swapErrorGeneric(String error) {
return '교환 오류: $error';
}
@override
String get swapChartUnavailable => '가격 불러오기 실패 · 탭하여 재시도';
@override
String swapChartMinMax(String minPrice, String maxPrice) {
return '24h 최저: $minPrice — 최고: $maxPrice';
}
@override
String get privacyPolicy => '개인정보 처리방침';
@override
String get privacyTitle => '우리는 아무것도 수집하지 않습니다';
@override
String get privacyGoodbye => '안녕';
@override
String get privacyKeepReading => '(계속 읽고 싶으면…)';
@override
String get privacyBody => '당신이 누구인지 모릅니다\n얼마를 가지고 있는지 모릅니다\n무엇을 하는지 모릅니다';
@override
String get privacyConclusion => '데이터를 보호하는 가장 좋은 방법은\n데이터를 갖지 않는 것입니다';
}
+29
View File
@@ -1277,4 +1277,33 @@ 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 swapChartMinMax(String minPrice, String maxPrice) {
return '24h Mín: $minPrice — Máx: $maxPrice';
}
@override
String get privacyPolicy => 'Política de privacidade';
@override
String get privacyTitle => 'NÃO RECOLHEMOS NADA';
@override
String get privacyGoodbye => 'ADEUS';
@override
String get privacyKeepReading => '(continue lendo se quiser…)';
@override
String get privacyBody =>
'Não sabemos quem você é\nNão sabemos quanto você tem\nNão sabemos o que você faz';
@override
String get privacyConclusion =>
'A melhor forma de proteger seus dados\né não tê-los';
}
+28
View File
@@ -1273,4 +1273,32 @@ class L10nRu extends L10n {
String swapErrorGeneric(String error) {
return 'Ошибка обмена: $error';
}
@override
String get swapChartUnavailable => 'Цена недоступна · Нажмите для повтора';
@override
String swapChartMinMax(String minPrice, String maxPrice) {
return '24h Мин: $minPrice — Макс: $maxPrice';
}
@override
String get privacyPolicy => 'Политика конфиденциальности';
@override
String get privacyTitle => 'МЫ НЕ СОБИРАЕМ НИЧЕГО';
@override
String get privacyGoodbye => 'ПОКА';
@override
String get privacyKeepReading => '(читай дальше, если хочешь…)';
@override
String get privacyBody =>
'Мы не знаем, кто ты\nМы не знаем, сколько у тебя\nМы не знаем, что ты делаешь';
@override
String get privacyConclusion =>
'Лучший способ защитить твои данные —\nне иметь их';
}
+28
View File
@@ -1274,4 +1274,32 @@ class L10nSw extends L10n {
String swapErrorGeneric(String error) {
return 'Kosa la ubadilishaji: $error';
}
@override
String get swapChartUnavailable => 'Bei haipatikani · Gusa kurudia';
@override
String swapChartMinMax(String minPrice, String maxPrice) {
return '24h Chini: $minPrice — Juu: $maxPrice';
}
@override
String get privacyPolicy => 'Sera ya faragha';
@override
String get privacyTitle => 'HATUKUSANYI CHOCHOTE';
@override
String get privacyGoodbye => 'KWAHERI';
@override
String get privacyKeepReading => '(endelea kusoma ukitaka…)';
@override
String get privacyBody =>
'Hatujui wewe ni nani\nHatujui una kiasi gani\nHatujui unafanya nini';
@override
String get privacyConclusion =>
'Njia bora ya kulinda data yako\nni kutokuwa nayo';
}
+26
View File
@@ -1245,4 +1245,30 @@ class L10nZh extends L10n {
String swapErrorGeneric(String error) {
return '兑换错误: $error';
}
@override
String get swapChartUnavailable => '价格不可用 · 点击重试';
@override
String swapChartMinMax(String minPrice, String maxPrice) {
return '24h 最低: $minPrice — 最高: $maxPrice';
}
@override
String get privacyPolicy => '隐私政策';
@override
String get privacyTitle => '我们什么都不收集';
@override
String get privacyGoodbye => '再见';
@override
String get privacyKeepReading => '(想继续看就看吧…)';
@override
String get privacyBody => '我们不知道你是谁\n我们不知道你有多少\n我们不知道你在做什么';
@override
String get privacyConclusion => '保护数据的最好方式\n就是不拥有它';
}
+9 -1
View File
@@ -573,5 +573,13 @@
"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",
"swapChartMinMax": "24h Mín: {minPrice} — Máx: {maxPrice}",
"privacyPolicy": "Política de privacidade",
"privacyTitle": "NÃO RECOLHEMOS NADA",
"privacyGoodbye": "ADEUS",
"privacyKeepReading": "(continue lendo se quiser…)",
"privacyBody": "Não sabemos quem você é\nNão sabemos quanto você tem\nNão sabemos o que você faz",
"privacyConclusion": "A melhor forma de proteger seus dados\né não tê-los"
}
+9 -1
View File
@@ -573,5 +573,13 @@
"swapSuccess": "Обмен завершён",
"swapErrorInsufficient": "Недостаточный баланс",
"swapErrorExpired": "Котировка истекла",
"swapErrorGeneric": "Ошибка обмена: {error}"
"swapErrorGeneric": "Ошибка обмена: {error}",
"swapChartUnavailable": "Цена недоступна · Нажмите для повтора",
"swapChartMinMax": "24h Мин: {minPrice} — Макс: {maxPrice}",
"privacyPolicy": "Политика конфиденциальности",
"privacyTitle": "МЫ НЕ СОБИРАЕМ НИЧЕГО",
"privacyGoodbye": "ПОКА",
"privacyKeepReading": "(читай дальше, если хочешь…)",
"privacyBody": "Мы не знаем, кто ты\nМы не знаем, сколько у тебя\nМы не знаем, что ты делаешь",
"privacyConclusion": "Лучший способ защитить твои данные —\nне иметь их"
}
+9 -1
View File
@@ -573,5 +573,13 @@
"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",
"swapChartMinMax": "24h Chini: {minPrice} — Juu: {maxPrice}",
"privacyPolicy": "Sera ya faragha",
"privacyTitle": "HATUKUSANYI CHOCHOTE",
"privacyGoodbye": "KWAHERI",
"privacyKeepReading": "(endelea kusoma ukitaka…)",
"privacyBody": "Hatujui wewe ni nani\nHatujui una kiasi gani\nHatujui unafanya nini",
"privacyConclusion": "Njia bora ya kulinda data yako\nni kutokuwa nayo"
}
+9 -1
View File
@@ -573,5 +573,13 @@
"swapSuccess": "兑换完成",
"swapErrorInsufficient": "余额不足",
"swapErrorExpired": "报价已过期",
"swapErrorGeneric": "兑换错误: {error}"
"swapErrorGeneric": "兑换错误: {error}",
"swapChartUnavailable": "价格不可用 · 点击重试",
"swapChartMinMax": "24h 最低: {minPrice} — 最高: {maxPrice}",
"privacyPolicy": "隐私政策",
"privacyTitle": "我们什么都不收集",
"privacyGoodbye": "再见",
"privacyKeepReading": "(想继续看就看吧…)",
"privacyBody": "我们不知道你是谁\n我们不知道你有多少\n我们不知道你在做什么",
"privacyConclusion": "保护数据的最好方式\n就是不拥有它"
}
+108 -37
View File
@@ -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,33 +96,40 @@ 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) {
if (!mounted) return;
final chartData = prices
.map((p) => p.priceUsd)
.where((price) => price.isFinite && price > 0)
.toList();
if (chartData.length >= 2) {
setState(() {
_chartData = prices.map((p) => p.priceUsd).toList();
_chartData = chartData;
_isLoadingChart = false;
});
} else {
setState(() {
_chartData = [];
_isLoadingChart = false;
_chartError = true;
});
}
} 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 +270,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 +714,43 @@ 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);
minMaxStr = l10n.swapChartMinMax(
'\$${fmt.format(minVal.round())}',
'\$${fmt.format(maxVal.round())}',
);
}
return Column(
children: [
// Price
// Precio centrado
Text(
priceStr,
style: const TextStyle(
@@ -736,18 +761,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 +804,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,
+131
View File
@@ -0,0 +1,131 @@
import 'package:flutter/material.dart';
import 'package:lucide_icons/lucide_icons.dart';
import 'package:elcaju/l10n/app_localizations.dart';
import '../../core/constants/colors.dart';
import '../../core/constants/dimensions.dart';
import '../../widgets/common/gradient_background.dart';
class PrivacyScreen extends StatelessWidget {
const PrivacyScreen({super.key});
@override
Widget build(BuildContext context) {
final l10n = L10n.of(context)!;
final conclusionLines = l10n.privacyConclusion
.split('\n')
.map((s) => s.trim())
.where((s) => s.isNotEmpty)
.toList();
return GradientBackground(
child: Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(LucideIcons.arrowLeft, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
title: Text(
l10n.privacyPolicy,
style: const TextStyle(
fontFamily: 'Inter',
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
),
body: SafeArea(
child: LayoutBuilder(
builder: (context, constraints) => SingleChildScrollView(
padding: const EdgeInsets.all(AppDimensions.paddingLarge),
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
l10n.privacyTitle,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
Text(
l10n.privacyGoodbye,
style: const TextStyle(
fontFamily: 'Inter',
fontSize: 24,
fontWeight: FontWeight.w800,
color: AppColors.primaryAction,
),
),
const SizedBox(height: 40),
Text(
l10n.privacyKeepReading,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 13,
fontStyle: FontStyle.italic,
color: AppColors.textSecondary.withValues(alpha: 0.5),
),
),
const SizedBox(height: 28),
...l10n.privacyBody.split('\n')
.map((s) => s.trim())
.where((s) => s.isNotEmpty)
.map(
(line) => Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Text(
line,
style: TextStyle(
fontFamily: 'Inter',
fontSize: 16,
color: AppColors.textSecondary.withValues(alpha: 0.85),
),
textAlign: TextAlign.center,
),
),
),
const SizedBox(height: 28),
...conclusionLines.asMap().entries.map((entry) {
final isLast = entry.key == conclusionLines.length - 1;
return Padding(
padding: EdgeInsets.only(bottom: isLast ? 0 : 8),
child: Text(
entry.value,
style: isLast
? const TextStyle(
fontFamily: 'Inter',
fontSize: 18,
fontWeight: FontWeight.w700,
color: Colors.white,
)
: TextStyle(
fontFamily: 'Inter',
fontSize: 15,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary.withValues(alpha: 0.7),
),
textAlign: TextAlign.center,
),
);
}),
],
),
),
),
),
),
),
),
);
}
}
+25 -16
View File
@@ -13,6 +13,7 @@ import '../../widgets/common/gradient_background.dart';
import '../../widgets/common/glass_card.dart';
import '../2_onboarding/backup_seed_screen.dart';
import 'mints_screen.dart';
import 'privacy_screen.dart';
import 'language_screen.dart';
import 'p2pk_keys_screen.dart';
import '../13_swap/swap_screen.dart';
@@ -120,7 +121,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
value: settingsProvider.pinEnabled,
onChanged: (value) =>
_togglePin(context, settingsProvider, value),
activeColor: AppColors.primaryAction,
activeThumbColor: AppColors.primaryAction,
),
),
_buildSettingTile(
@@ -165,6 +166,16 @@ class _SettingsScreenState extends State<SettingsScreen> {
title: l10n.about,
onTap: () => _showAboutDialog(context),
),
_buildSettingTile(
icon: LucideIcons.shield,
title: l10n.privacyPolicy,
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (_) => const PrivacyScreen(),
),
),
),
_buildSettingTile(
icon: LucideIcons.github,
title: 'GitHub',
@@ -486,7 +497,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
// Obtener mnemonic
final mnemonic = await settingsProvider.getMnemonic();
if (mnemonic == null) {
if (mounted) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.mnemonicNotFound),
@@ -498,7 +509,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
}
// Navegar a BackupSeedScreen
if (mounted) {
if (context.mounted) {
Navigator.push(
context,
MaterialPageRoute(
@@ -519,7 +530,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
final pin = await _showCreatePinDialog(context);
if (pin != null && pin.length == 4) {
await settingsProvider.setPin(pin);
if (mounted) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.pinActivated),
@@ -533,7 +544,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
final verified = await _verifyPinDialog(context, settingsProvider);
if (verified) {
await settingsProvider.removePin();
if (mounted) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.pinDeactivated),
@@ -562,6 +573,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (firstPin == null) return null;
// Confirmar PIN
if (!context.mounted) return null;
final confirmPin = await showDialog<String>(
context: context,
barrierDismissible: false,
@@ -574,7 +586,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (confirmPin == null) return null;
if (firstPin != confirmPin) {
if (mounted) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.pinMismatch),
@@ -605,7 +617,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
if (settingsProvider.verifyPin(pin)) {
return true;
} else {
if (mounted) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(l10n.incorrectPin),
@@ -746,7 +758,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
try {
await launchUrl(url, mode: LaunchMode.externalApplication);
} catch (_) {
if (mounted) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.couldNotOpenLink),
@@ -813,7 +825,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
await walletProvider.deleteDatabase();
await settingsProvider.deleteWallet();
if (mounted) {
if (context.mounted) {
// Navegar a welcome screen
Navigator.pushNamedAndRemoveUntil(
context,
@@ -822,7 +834,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
);
}
} catch (e) {
if (mounted) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(L10n.of(context)!.deleteError(e.toString())),
@@ -1007,7 +1019,9 @@ class _PinDialogState extends State<_PinDialog> {
if (_pin.length == 4) {
// PIN completo, cerrar con resultado
Future.delayed(const Duration(milliseconds: 200), () {
Navigator.pop(context, _pin.join());
if (mounted) {
Navigator.pop(context, _pin.join());
}
});
}
}
@@ -1825,7 +1839,6 @@ class _RecoverTokensModalState extends State<_RecoverTokensModal> {
// Escanear todos los mints (retorna Map<String, Map<String, BigInt>>)
final results = await walletProvider.restoreAllMints();
BigInt totalRecovered = BigInt.zero;
int mintsScanned = 0;
int mintsWithError = 0;
final recoveredDetails = <String>[];
@@ -1833,15 +1846,12 @@ class _RecoverTokensModalState extends State<_RecoverTokensModal> {
for (final mintEntry in results.entries) {
final unitBalances = mintEntry.value;
bool hasError = false;
BigInt mintTotal = BigInt.zero;
for (final unitEntry in unitBalances.entries) {
final unit = unitEntry.key;
final balance = unitEntry.value;
if (balance < BigInt.zero) {
hasError = true;
} else if (balance > BigInt.zero) {
mintTotal += balance;
final formatted = UnitFormatter.formatBalance(balance, unit);
final label = UnitFormatter.getUnitLabel(unit);
recoveredDetails.add('$formatted $label');
@@ -1852,7 +1862,6 @@ class _RecoverTokensModalState extends State<_RecoverTokensModal> {
mintsWithError++;
} else {
mintsScanned++;
totalRecovered += mintTotal;
}
}