Make layouts adapt to larger screens
What changed, and why it matters
This commit is a routine UI polish for the Skylight Monero wallet. It makes screens adapt better to large monitors and tablets, adds a desktop copy-address button, fixes a macOS notification bug, and replaces a full-screen transaction details page with a popup dialog. There is no clear security vulnerability in the changes, but one small behavior change is worth noting: the old receive screen copied the address silently when tapped, while the new code shows a confirmation message. Most of the diff is reformatting and layout work.
No security action required. Treat as a normal UI/UX improvement. If reviewing further, verify that the new TxDetailsDialog does not expose sensitive transaction data in a way the old screen did not, and that the macOS notification path works as intended.
Security signals we found
macOS added to desktop incoming-transaction notification platform check
receive screen now guards brightness restore and share button by mobile platform
address copy action now shows user feedback instead of silent clipboard write
transaction details moved from full screen route to dialog widget
Evidence from the diff
The diff is dominated by responsive-layout refactoring: wrapping screens in Container(constraints: BoxConstraints(maxWidth: …)), adding a DeviceType enum and breakpoints in wallet_home.dart, converting tx_details from a named route/screen into a dialog widget, and removing the /tx_details route. A few functional changes appear: (1) desktop notifications for incoming transactions now also trigger on macOS (Platform.isMacOS added); (2) the receive screen now shows a SnackBar when the address is copied and offers a desktop-specific copy button; (3) screen-brightness restoration on dispose is guarded by Platform.isAndroid || Platform.isIOS; (4) localization keys were consolidated (addressBookAddressCopied -> addressCopied, plus new close/copy strings). No cryptographic, networking, or storage logic was altered in a security-relevant way.
Changed components
lib/screens/wallet_home.dartlib/screens/receive.dartlib/models/wallet_model.dartlib/widgets/tx_details.dartlib/main.dartlib/screens/address_book.dartlib/screens/confirm_send.dartlib/screens/send.dartlib/screens/settings.dartInspect captured patch +1320 / −1366
diff --git a/analysis_options.yaml b/analysis_options.yaml
index 0d29021..118aca6 100644
--- a/analysis_options.yaml
+++ b/analysis_options.yaml
@@ -24,5 +24,8 @@ linter:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
+formatter:
+ page_width: 100
+
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index eab8391..a41d2e7 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -2,12 +2,15 @@
"@@locale": "en",
"continueText": "Continue",
"cancel": "Cancel",
+ "close": "Close",
"unknownError": "Unknown error.",
"warning": "Warning",
"amount": "Amount",
"networkFee": "Network Fee",
"address": "Address",
"pending": "Pending",
+ "copy": "Copy",
+ "addressCopied": "Address copied to clipboard",
"fieldEmptyError": "This field cannot be empty.",
"welcomeTitle": "Welcome!",
"welcomeDescription": "Skylight is one of the simplest Monero wallets. We will help you set up a wallet and connect to a server.",
@@ -138,7 +141,6 @@
"addressBookContactName": "Contact Name",
"addressBookUpdate": "Update",
"addressBookSave": "Save",
- "addressBookAddressCopied": "Address copied to clipboard",
"sendSelectFromAddressBook": "Select from Address Book",
"sendSelectedContact": "Selected contact",
"sendClearSelectedContact": "Clear selected contact",
diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart
index ec10393..8f1915a 100644
--- a/lib/l10n/app_localizations.dart
+++ b/lib/l10n/app_localizations.dart
@@ -62,8 +62,7 @@ import 'app_localizations_pt.dart';
/// be consistent with the languages listed in the AppLocalizations.supportedLocales
/// property.
abstract class AppLocalizations {
- AppLocalizations(String locale)
- : localeName = intl.Intl.canonicalizedLocale(locale.toString());
+ AppLocalizations(String locale) : localeName = intl.Intl.canonicalizedLocale(locale.toString());
final String localeName;
@@ -71,8 +70,7 @@ abstract class AppLocalizations {
return Localizations.of<AppLocalizations>(context, AppLocalizations);
}
- static const LocalizationsDelegate<AppLocalizations> delegate =
- _AppLocalizationsDelegate();
+ static const LocalizationsDelegate<AppLocalizations> delegate = _AppLocalizationsDelegate();
/// A list of this localizations delegate along with the default localizations
/// delegates.
@@ -93,10 +91,7 @@ abstract class AppLocalizations {
];
/// A list of this localizations delegate's supported locales.
- static const List<Locale> supportedLocales = <Locale>[
- Locale('en'),
- Locale('pt'),
- ];
+ static const List<Locale> supportedLocales = <Locale>[Locale('en'), Locale('pt')];
/// No description provided for @continueText.
///
@@ -110,6 +105,12 @@ abstract class AppLocalizations {
/// **'Cancel'**
String get cancel;
+ /// No description provided for @close.
+ ///
+ /// In en, this message translates to:
+ /// **'Close'**
+ String get close;
+
/// No description provided for @unknownError.
///
/// In en, this message translates to:
@@ -146,6 +147,18 @@ abstract class AppLocalizations {
/// **'Pending'**
String get pending;
+ /// No description provided for @copy.
+ ///
+ /// In en, this message translates to:
+ /// **'Copy'**
+ String get copy;
+
+ /// No description provided for @addressCopied.
+ ///
+ /// In en, this message translates to:
+ /// **'Address copied to clipboard'**
+ String get addressCopied;
+
/// No description provided for @fieldEmptyError.
///
/// In en, this message translates to:
@@ -884,12 +897,6 @@ abstract class AppLocalizations {
/// **'Save'**
String get addressBookSave;
- /// No description provided for @addressBookAddressCopied.
- ///
- /// In en, this message translates to:
- /// **'Address copied to clipboard'**
- String get addressBookAddressCopied;
-
/// No description provided for @sendSelectFromAddressBook.
///
/// In en, this message translates to:
@@ -963,8 +970,7 @@ abstract class AppLocalizations {
String get sendFailedToGetFeesError;
}
-class _AppLocalizationsDelegate
- extends LocalizationsDelegate<AppLocalizations> {
+class _AppLocalizationsDelegate extends LocalizationsDelegate<AppLocalizations> {
const _AppLocalizationsDelegate();
@override
@@ -973,8 +979,7 @@ class _AppLocalizationsDelegate
}
@override
- bool isSupported(Locale locale) =>
- <String>['en', 'pt'].contains(locale.languageCode);
+ bool isSupported(Locale locale) => <String>['en', 'pt'].contains(locale.languageCode);
@override
bool shouldReload(_AppLocalizationsDelegate old) => false;
diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart
index 1a83180..912b61e 100644
--- a/lib/l10n/app_localizations_en.dart
+++ b/lib/l10n/app_localizations_en.dart
@@ -14,6 +14,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get cancel => 'Cancel';
+ @override
+ String get close => 'Close';
+
@override
String get unknownError => 'Unknown error.';
@@ -32,6 +35,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get pending => 'Pending';
+ @override
+ String get copy => 'Copy';
+
+ @override
+ String get addressCopied => 'Address copied to clipboard';
+
@override
String get fieldEmptyError => 'This field cannot be empty.';
@@ -59,12 +68,10 @@ class AppLocalizationsEn extends AppLocalizations {
String get connectionSetupTitle => 'Connection Setup';
@override
- String get connectionSetupDescription =>
- 'Let\'s setup a connection with LWS.';
+ String get connectionSetupDescription => 'Let\'s setup a connection with LWS.';
@override
- String get connectionSetupAddressHint =>
- 'e.g. 192.168.1.1:18090 or example.com:18090';
+ String get connectionSetupAddressHint => 'e.g. 192.168.1.1:18090 or example.com:18090';
@override
String get connectionSetupProxyPortLabel => 'HTTP Proxy Port (optional)';
@@ -224,8 +231,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get sendSendButton => 'Send';
@override
- String get sendTransactionSuccessfullySent =>
- 'Transaction successfully sent!';
+ String get sendTransactionSuccessfullySent => 'Transaction successfully sent!';
@override
String get sendOpenAliasResolveError => 'Invalid OpenAlias.';
@@ -401,8 +407,7 @@ class AppLocalizationsEn extends AppLocalizations {
String get addressBookNoContacts => 'No contacts yet';
@override
- String get addressBookNoContactsDescription =>
- 'Add your first contact by tapping the + button';
+ String get addressBookNoContactsDescription => 'Add your first contact by tapping the + button';
@override
String get addressBookNoSearchResults => 'No contacts found';
@@ -422,9 +427,6 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get addressBookSave => 'Save';
- @override
- String get addressBookAddressCopied => 'Address copied to clipboard';
-
@override
String get sendSelectFromAddressBook => 'Select from Address Book';
diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart
index 9d67f5d..d8913f5 100644
--- a/lib/l10n/app_localizations_pt.dart
+++ b/lib/l10n/app_localizations_pt.dart
@@ -14,6 +14,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get cancel => 'Cancelar';
+ @override
+ String get close => 'Fechar';
+
@override
String get unknownError => 'Erro desconhecido.';
@@ -32,6 +35,12 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get pending => 'Pendente';
+ @override
+ String get copy => 'Copiar';
+
+ @override
+ String get addressCopied => 'Endereço copiado para a área de transferência';
+
@override
String get fieldEmptyError => 'Este campo não pode ficar vazio.';
@@ -59,12 +68,10 @@ class AppLocalizationsPt extends AppLocalizations {
String get connectionSetupTitle => 'Configuração da Conexão';
@override
- String get connectionSetupDescription =>
- 'Vamos configurar uma conexão com o LWS.';
+ String get connectionSetupDescription => 'Vamos configurar uma conexão com o LWS.';
@override
- String get connectionSetupAddressHint =>
- 'ex: 192.168.1.1:18090 ou exemplo.com:18090';
+ String get connectionSetupAddressHint => 'ex: 192.168.1.1:18090 ou exemplo.com:18090';
@override
String get connectionSetupProxyPortLabel => 'Porta do Proxy HTTP (opcional)';
@@ -224,8 +231,7 @@ class AppLocalizationsPt extends AppLocalizations {
String get sendSendButton => 'Enviar';
@override
- String get sendTransactionSuccessfullySent =>
- 'Transação enviada com sucesso!';
+ String get sendTransactionSuccessfullySent => 'Transação enviada com sucesso!';
@override
String get sendOpenAliasResolveError => 'OpenAlias inválido.';
@@ -401,8 +407,7 @@ class AppLocalizationsPt extends AppLocalizations {
String get addressBookNoContacts => 'Nenhum contato ainda';
@override
- String get addressBookNoContactsDescription =>
- 'Adicione seu primeiro contato tocando no botão +';
+ String get addressBookNoContactsDescription => 'Adicione seu primeiro contato tocando no botão +';
@override
String get addressBookNoSearchResults => 'Nenhum contato encontrado';
@@ -422,10 +427,6 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get addressBookSave => 'Salvar';
- @override
- String get addressBookAddressCopied =>
- 'Endereço copiado para a área de transferência';
-
@override
String get sendSelectFromAddressBook => 'Selecionar da Lista de Contatos';
diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb
index c5ded69..652d684 100644
--- a/lib/l10n/app_pt.arb
+++ b/lib/l10n/app_pt.arb
@@ -2,12 +2,15 @@
"@@locale": "pt",
"continueText": "Continuar",
"cancel": "Cancelar",
+ "close": "Fechar",
"unknownError": "Erro desconhecido.",
"warning": "Atenção",
"amount": "Valor",
"networkFee": "Taxa da Rede",
"address": "Endereço",
"pending": "Pendente",
+ "copy": "Copiar",
+ "addressCopied": "Endereço copiado para a área de transferência",
"fieldEmptyError": "Este campo não pode ficar vazio.",
"welcomeTitle": "Bem-vindo!",
"welcomeDescription": "A Skylight é uma das mais simples carteiras de Monero. Nós o ajudaremos a configurar uma carteira e se conectar à um servidor.",
@@ -138,7 +141,6 @@
"addressBookContactName": "Nome do Contato",
"addressBookUpdate": "Atualizar",
"addressBookSave": "Salvar",
- "addressBookAddressCopied": "Endereço copiado para a área de transferência",
"sendSelectFromAddressBook": "Selecionar da Lista de Contatos",
"sendSelectedContact": "Contato selecionado",
"sendClearSelectedContact": "Limpar contato selecionado",
diff --git a/lib/main.dart b/lib/main.dart
index 5846366..a30c22e 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -27,7 +27,6 @@ import 'package:skylight_wallet/screens/connection_setup.dart';
import 'package:skylight_wallet/screens/generate_seed.dart';
import 'package:skylight_wallet/screens/receive.dart';
import 'package:skylight_wallet/screens/send.dart';
-import 'package:skylight_wallet/screens/tx_details.dart';
import 'package:skylight_wallet/screens/create_wallet.dart';
import 'package:skylight_wallet/screens/restore_wallet.dart';
import 'package:skylight_wallet/screens/restore_warning.dart';
@@ -84,21 +83,14 @@ class MyApp extends StatelessWidget {
loadExistingWalletIfExists(wallet),
]),
builder: (context, snapshot) {
- if (snapshot.connectionState == ConnectionState.done &&
- snapshot.data != null) {
- final sharedPreferences =
- snapshot.data![0] as SharedPreferences;
+ if (snapshot.connectionState == ConnectionState.done && snapshot.data != null) {
+ final sharedPreferences = snapshot.data![0] as SharedPreferences;
final walletExists = snapshot.data![1] as bool;
- final theme =
- sharedPreferences.getString(SharedPreferencesKeys.theme) ??
- 'system';
+ final theme = sharedPreferences.getString(SharedPreferencesKeys.theme) ?? 'system';
final appLockEnabled =
- sharedPreferences.getBool(
- SharedPreferencesKeys.appLockEnabled,
- ) ??
- false;
+ sharedPreferences.getBool(SharedPreferencesKeys.appLockEnabled) ?? false;
final initialRoute = walletExists
? appLockEnabled
@@ -112,12 +104,9 @@ class MyApp extends StatelessWidget {
return MaterialApp(
title: 'Skylight Monero Wallet',
- localizationsDelegates:
- AppLocalizations.localizationsDelegates,
+ localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
- theme: ThemeData(
- colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
- ),
+ theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue)),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
@@ -130,9 +119,7 @@ class MyApp extends StatelessWidget {
? ThemeMode.light
: ThemeMode.system,
initialRoute: initialRoute,
- locale: Locale.fromSubtags(
- languageCode: languageProvider.language,
- ),
+ locale: Locale.fromSubtags(languageCode: languageProvider.language),
routes: {
'/welcome': (context) => WelcomeScreen(),
'/connection_setup': (context) => ConnectionSetupScreen(),
@@ -150,7 +137,6 @@ class MyApp extends StatelessWidget {
'/confirm_send': (context) => ConfirmSendScreen(),
'/scan_qr': (context) => ScanQrScreen(),
'/receive': (context) => ReceiveScreen(),
- '/tx_details': (context) => TxDetailsScreen(),
'/address_book': (context) => AddressBookScreen(),
'/terms_of_service': (context) => TermsOfService(),
'/privacy_policy': (context) => PrivacyPolicy(),
@@ -165,9 +151,7 @@ class MyApp extends StatelessWidget {
return MaterialApp(
title: 'Skylight Monero Wallet',
- theme: ThemeData(
- colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
- ),
+ theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue)),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue,
diff --git a/lib/models/wallet_model.dart b/lib/models/wallet_model.dart
index b96765a..4ba2163 100644
--- a/lib/models/wallet_model.dart
+++ b/lib/models/wallet_model.dart
@@ -108,10 +108,8 @@ class TxRecipient {
Map<String, dynamic> toJson() => {'address': address, 'amount': amount};
- factory TxRecipient.fromJson(Map<String, dynamic> json) => TxRecipient(
- json['address'] as String,
- (json['amount'] as num).toDouble(),
- );
+ factory TxRecipient.fromJson(Map<String, dynamic> json) =>
+ TxRecipient(json['address'] as String, (json['amount'] as num).toDouble());
}
class LWSConnectionDetails {
@@ -129,9 +127,7 @@ class LWSConnectionDetails {
}
class WalletModel with ChangeNotifier {
- final _w2WalletManager = Monero()
- .walletManagerFactory()
- .getLWSFWalletManager();
+ final _w2WalletManager = Monero().walletManagerFactory().getLWSFWalletManager();
Wallet2Wallet? _w2Wallet;
Wallet2TransactionHistory? _w2TxHistory;
@@ -164,8 +160,7 @@ class WalletModel with ChangeNotifier {
bool get usingTor => _connectionUseTor;
bool? get serverSupportsSubaddresses => _serverSupportsSubaddresses;
int? get unusedSubaddressIndex => _unusedSubaddressIndex;
- bool? get unusedSubaddressIndexIsSupported =>
- _unusedSubaddressIndexIsSupported;
+ bool? get unusedSubaddressIndexIsSupported => _unusedSubaddressIndexIsSupported;
WalletModel() {
_startTimers();
@@ -230,10 +225,7 @@ class WalletModel with ChangeNotifier {
Future<void> loadAllStats() async {
if (_w2Wallet == null) {
- log(
- LogLevel.warn,
- 'Attempted to load all stats but there is no wallet open.',
- );
+ log(LogLevel.warn, 'Attempted to load all stats but there is no wallet open.');
return;
}
@@ -269,7 +261,7 @@ class WalletModel with ChangeNotifier {
_txHistory = await _getFullTxHistory();
// Notify new transactions on desktop
- if ((Platform.isLinux || Platform.isWindows) &&
+ if ((Platform.isLinux || Platform.isWindows || Platform.isMacOS) &&
_isConnected &&
_isSynced &&
_syncedHeight is int &&
@@ -277,8 +269,7 @@ class WalletModel with ChangeNotifier {
for (int i = 0; i < txCountDiff; i++) {
final tx = _txHistory[i];
- if (tx.direction == consts.txDirectionIncoming &&
- tx.timestamp > _sessionStartedAt) {
+ if (tx.direction == consts.txDirectionIncoming && tx.timestamp > _sessionStartedAt) {
NotificationService().showIncomingTxNotification(tx.amount);
// Only notify one new transaction
break;
@@ -297,46 +288,26 @@ class WalletModel with ChangeNotifier {
}
Future<void> persistCurrentConnection() async {
- await SharedPreferencesService.set(
- SharedPreferencesKeys.connectionAddress,
- _connectionAddress,
- );
+ await SharedPreferencesService.set(SharedPreferencesKeys.connectionAddress, _connectionAddress);
await SharedPreferencesService.set(
SharedPreferencesKeys.connectionProxyPort,
_connectionProxyPort,
);
- await SharedPreferencesService.set(
- SharedPreferencesKeys.connectionUseTor,
- _connectionUseTor,
- );
- await SharedPreferencesService.set(
- SharedPreferencesKeys.connectionUseSsl,
- _connectionUseSsl,
- );
+ await SharedPreferencesService.set(SharedPreferencesKeys.connectionUseTor, _connectionUseTor);
+ await SharedPreferencesService.set(SharedPreferencesKeys.connectionUseSsl, _connectionUseSsl);
}
Future<LWSConnectionDetails> getPersistedConnection() async {
return LWSConnectionDetails(
address:
- await SharedPreferencesService.get<String>(
- SharedPreferencesKeys.connectionAddress,
- ) ??
- '',
+ await SharedPreferencesService.get<String>(SharedPreferencesKeys.connectionAddress) ?? '',
proxyPort:
- await SharedPreferencesService.get<String>(
- SharedPreferencesKeys.connectionProxyPort,
- ) ??
+ await SharedPreferencesService.get<String>(SharedPreferencesKeys.connectionProxyPort) ??
'',
useTor:
- await SharedPreferencesService.get<bool>(
- SharedPreferencesKeys.connectionUseTor,
- ) ??
- false,
+ await SharedPreferencesService.get<bool>(SharedPreferencesKeys.connectionUseTor) ?? false,
useSsl:
- await SharedPreferencesService.get<bool>(
- SharedPreferencesKeys.connectionUseSsl,
- ) ??
- false,
+ await SharedPreferencesService.get<bool>(SharedPreferencesKeys.connectionUseSsl) ?? false,
);
}
@@ -362,10 +333,7 @@ class WalletModel with ChangeNotifier {
}
Future<int> getPersistedTxHistoryCount() async {
- return await SharedPreferencesService.get<int>(
- SharedPreferencesKeys.txHistoryCount,
- ) ??
- 0;
+ return await SharedPreferencesService.get<int>(SharedPreferencesKeys.txHistoryCount) ?? 0;
}
void setConnection({
@@ -510,10 +478,7 @@ class WalletModel with ChangeNotifier {
Future<void> loadPersistedUnusedSubaddressIndex() async {
_unusedSubaddressIndex =
- await SharedPreferencesService.get<int>(
- SharedPreferencesKeys.unusedSubaddressIndex,
- ) ??
- 1;
+ await SharedPreferencesService.get<int>(SharedPreferencesKeys.unusedSubaddressIndex) ?? 1;
_unusedSubaddressIndexIsSupported =
await SharedPreferencesService.get<bool>(
SharedPreferencesKeys.unusedSubaddressIndexIsSupported,
@@ -567,11 +532,7 @@ class WalletModel with ChangeNotifier {
httpStatus = response.statusCode;
} else {
final response = await http
- .post(
- url,
- headers: {'Content-Type': 'application/json'},
- body: body,
- )
+ .post(url, headers: {'Content-Type': 'application/json'}, body: body)
.timeout(Duration(seconds: 5));
httpStatus = response.statusCode;
@@ -580,10 +541,7 @@ class WalletModel with ChangeNotifier {
break;
} catch (e) {
if (i == 2) {
- log(
- LogLevel.warn,
- 'Failed to check subaddress support after ${i + 1} attempts.',
- );
+ log(LogLevel.warn, 'Failed to check subaddress support after ${i + 1} attempts.');
log(LogLevel.warn, 'Error: $e');
rethrow;
@@ -621,9 +579,7 @@ class WalletModel with ChangeNotifier {
),
Isolate.run(
// ignore: deprecated_member_use
- () => monero.TransactionHistory_refresh(
- Pointer.fromAddress(historyFfiAddr),
- ),
+ () => monero.TransactionHistory_refresh(Pointer.fromAddress(historyFfiAddr)),
),
]);
@@ -652,19 +608,13 @@ class WalletModel with ChangeNotifier {
var w2RestoreHeight = _w2Wallet!.getRefreshFromBlockHeight();
- log(
- LogLevel.info,
- 'Wallet_getRefreshFromBlockHeight result: $w2RestoreHeight',
- );
+ log(LogLevel.info, 'Wallet_getRefreshFromBlockHeight result: $w2RestoreHeight');
if (w2RestoreHeight > 0) {
return w2RestoreHeight;
}
- return await SharedPreferencesService.get<int>(
- SharedPreferencesKeys.walletRestoreHeight,
- ) ??
- 0;
+ return await SharedPreferencesService.get<int>(SharedPreferencesKeys.walletRestoreHeight) ?? 0;
}
Future<int> getCurrentHeight() async {
@@ -674,9 +624,7 @@ class WalletModel with ChangeNotifier {
final height = await Isolate.run(() {
// ignore: deprecated_member_use
- return monero.WalletManager_blockchainHeight(
- Pointer.fromAddress(wmFfiAddr),
- );
+ return monero.WalletManager_blockchainHeight(Pointer.fromAddress(wmFfiAddr));
});
log(LogLevel.info, 'WalletManager_blockchainHeight result: $height');
@@ -779,8 +727,7 @@ class WalletModel with ChangeNotifier {
restoreHeight: restoreHeight,
password: walletPassword,
);
- } else if (polyseedWallet.errorString() == '' &&
- polyseedWallet.status() == 0) {
+ } else if (polyseedWallet.errorString() == '' && polyseedWallet.status() == 0) {
_w2Wallet = await _getWalletFromPolyseed(
mnemonic: mnemonic,
restoreHeight: restoreHeight,
@@ -795,10 +742,7 @@ class WalletModel with ChangeNotifier {
} else if (_w2Wallet == null) {
log(LogLevel.error, 'Something went wrong when restoring from mnemonic.');
log(LogLevel.error, 'Legacy wallet error: ${legacyWallet.errorString()}');
- log(
- LogLevel.error,
- 'Polyseed wallet error: ${polyseedWallet.errorString()}',
- );
+ log(LogLevel.error, 'Polyseed wallet error: ${polyseedWallet.errorString()}');
throw Exception('Something went wrong.');
}
@@ -816,8 +760,7 @@ class WalletModel with ChangeNotifier {
final password = await getWalletPassword();
if (password == null) {
- final errorMsg =
- 'Failed to open existing wallet: could not get password.';
+ final errorMsg = 'Failed to open existing wallet: could not get password.';
log(LogLevel.error, errorMsg);
throw Exception(errorMsg);
}
@@ -871,23 +814,13 @@ class WalletModel with ChangeNotifier {
await File(path).delete();
await SharedPreferencesService.remove(SharedPreferencesKeys.txHistoryCount);
- await SharedPreferencesService.remove(
- SharedPreferencesKeys.walletRestoreHeight,
- );
+ await SharedPreferencesService.remove(SharedPreferencesKeys.walletRestoreHeight);
await SharedPreferencesService.remove(SharedPreferencesKeys.appLockEnabled);
- await SharedPreferencesService.remove(
- SharedPreferencesKeys.pendingOutgoingTxs,
- );
- await SharedPreferencesService.remove(
- SharedPreferencesKeys.serverSupportsSubaddresses,
- );
+ await SharedPreferencesService.remove(SharedPreferencesKeys.pendingOutgoingTxs);
+ await SharedPreferencesService.remove(SharedPreferencesKeys.serverSupportsSubaddresses);
await SharedPreferencesService.remove(SharedPreferencesKeys.contacts);
- await SharedPreferencesService.remove(
- SharedPreferencesKeys.unusedSubaddressIndex,
- );
- await SharedPreferencesService.remove(
- SharedPreferencesKeys.unusedSubaddressIndexIsSupported,
- );
+ await SharedPreferencesService.remove(SharedPreferencesKeys.unusedSubaddressIndex);
+ await SharedPreferencesService.remove(SharedPreferencesKeys.unusedSubaddressIndexIsSupported);
}
Future<bool> hasExistingWallet() async {
@@ -943,9 +876,7 @@ class WalletModel with ChangeNotifier {
_syncedHeight = await Isolate.run(
// ignore: deprecated_member_use
- () => monero.Wallet_blockChainHeight(
- Pointer<Void>.fromAddress(walletFfiAddr),
- ),
+ () => monero.Wallet_blockChainHeight(Pointer<Void>.fromAddress(walletFfiAddr)),
);
log(LogLevel.info, 'Wallet_blockChainHeight result: $_syncedHeight');
@@ -1021,10 +952,7 @@ class WalletModel with ChangeNotifier {
log(LogLevel.info, ' accountIndex: 0');
log(LogLevel.info, ' addressIndex: $subaddrIndex');
- final subaddress = _w2Wallet!.address(
- accountIndex: 0,
- addressIndex: subaddrIndex,
- );
+ final subaddress = _w2Wallet!.address(accountIndex: 0, addressIndex: subaddrIndex);
log(LogLevel.info, 'Wallet_address result: $subaddress');
@@ -1045,10 +973,7 @@ class WalletModel with ChangeNotifier {
final mixinCount = 15;
final subaddrAccount = 0;
- log(
- LogLevel.info,
- 'Calling Wallet_createTransactionMultDest with parameters:',
- );
+ log(LogLevel.info, 'Calling Wallet_createTransactionMultDest with parameters:');
log(LogLevel.info, ' w2WalletFfiAddr: $w2WalletFfiAddr');
log(LogLevel.info, ' isSweepAll: $isSweepAll');
log(LogLevel.info, ' dstAddr: $dstAddr');
@@ -1077,20 +1002,14 @@ class WalletModel with ChangeNotifier {
final pendingTx = MoneroPendingTransaction(txPointer);
if (pendingTx.errorString() != '') {
- log(
- LogLevel.error,
- 'Failed to create transaction: ${pendingTx.errorString()}',
- );
+ log(LogLevel.error, 'Failed to create transaction: ${pendingTx.errorString()}');
throw Exception(pendingTx.errorString());
}
return pendingTx;
}
- Future<void> commitTx(
- MoneroPendingTransaction tx,
- String destinationAddress,
- ) async {
+ Future<void> commitTx(MoneroPendingTransaction tx, String destinationAddress) async {
final txFfiAddr = tx.ffiAddress();
final filename = '';
@@ -1118,10 +1037,7 @@ class WalletModel with ChangeNotifier {
throw FormatException(errorMsg);
}
- final recipient = TxRecipient(
- destinationAddress,
- doubleAmountFromInt(tx.amount()),
- );
+ final recipient = TxRecipient(destinationAddress, doubleAmountFromInt(tx.amount()));
final TxDetails txDetails = TxDetails(
index: null,
@@ -1146,10 +1062,7 @@ class WalletModel with ChangeNotifier {
Future<String> resolveOpenAlias(String address) async {
final dnssecValid = true;
- log(
- LogLevel.info,
- 'Calling WalletManager_resolveOpenAlias with parameters:',
- );
+ log(LogLevel.info, 'Calling WalletManager_resolveOpenAlias with parameters:');
log(LogLevel.info, ' address: $address');
log(LogLevel.info, ' dnssecValid: $dnssecValid');
@@ -1164,10 +1077,7 @@ class WalletModel with ChangeNotifier {
),
);
- log(
- LogLevel.info,
- 'WalletManager_resolveOpenAlias result: $resolvedAddress',
- );
+ log(LogLevel.info, 'WalletManager_resolveOpenAlias result: $resolvedAddress');
return resolvedAddress;
}
@@ -1187,23 +1097,15 @@ class WalletModel with ChangeNotifier {
Future<void> _persistPendingOutgoingTxs(List<TxDetails> txs) async {
final txsJson = txs.map((tx) => json.encode(tx)).toList();
final prefs = await SharedPreferences.getInstance();
- await prefs.setStringList(
- SharedPreferencesKeys.pendingOutgoingTxs,
- txsJson,
- );
+ await prefs.setStringList(SharedPreferencesKeys.pendingOutgoingTxs, txsJson);
}
Future<List<TxDetails>> _getPendingOutgoingTxs() async {
final prefs = await SharedPreferences.getInstance();
- final txsJson =
- prefs.getStringList(SharedPreferencesKeys.pendingOutgoingTxs) ?? [];
+ final txsJson = prefs.getStringList(SharedPreferencesKeys.pendingOutgoingTxs) ?? [];
final txs = txsJson
- .map(
- (jsonString) => TxDetails.fromJson(
- json.decode(jsonString) as Map<String, dynamic>,
- ),
- )
+ .map((jsonString) => TxDetails.fromJson(json.decode(jsonString) as Map<String, dynamic>))
.toList();
return txs;
@@ -1212,9 +1114,7 @@ class WalletModel with ChangeNotifier {
Future<double> getPendingOutgoingTxsAmountSum() async {
final txs = await _getPendingOutgoingTxs();
- final amountSum = txs
- .map((tx) => tx.amount + tx.fee)
- .reduce((value, el) => value + el);
+ final amountSum = txs.map((tx) => tx.amount + tx.fee).reduce((value, el) => value + el);
return amountSum;
}
diff --git a/lib/screens/address_book.dart b/lib/screens/address_book.dart
index bbff103..0d31cf8 100644
--- a/lib/screens/address_book.dart
+++ b/lib/screens/address_book.dart
@@ -45,26 +45,27 @@ class _AddressBookScreenState extends State<AddressBookScreen> {
showDialog(
context: context,
- builder: (context) => AlertDialog(
- title: Text(i18n.addressBookDeleteContact),
- content: Text(i18n.addressBookDeleteContactConfirmation(contact.name)),
- actions: [
- TextButton(
- onPressed: () => Navigator.of(context).pop(),
- child: Text(i18n.cancel),
- ),
- FilledButton(
- onPressed: () {
- Provider.of<ContactModel>(
- context,
- listen: false,
- ).deleteContact(contact.id);
- Navigator.of(context).pop();
- },
- child: Text(i18n.addressBookDelete),
- ),
- ],
- ),
+ builder: (context) {
+ final screenWidth = MediaQuery.of(context).size.width;
+ final dialogWidth = screenWidth.clamp(0.0, 400.0);
+
+ return AlertDialog(
+ constraints: BoxConstraints.tightFor(width: dialogWidth),
+ insetPadding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 24.0),
+ title: Text(i18n.addressBookDeleteContact),
+ content: Text(i18n.addressBookDeleteContactConfirmation(contact.name)),
+ actions: [
+ TextButton(onPressed: () => Navigator.of(context).pop(), child: Text(i18n.cancel)),
+ FilledButton(
+ onPressed: () {
+ Provider.of<ContactModel>(context, listen: false).deleteContact(contact.id);
+ Navigator.of(context).pop();
+ },
+ child: Text(i18n.addressBookDelete),
+ ),
+ ],
+ );
+ },
);
}
@@ -73,39 +74,34 @@ class _AddressBookScreenState extends State<AddressBookScreen> {
final i18n = AppLocalizations.of(context)!;
return Scaffold(
- appBar: AppBar(
- title: Text(i18n.addressBookTitle),
- actions: [
- IconButton(
- onPressed: _showAddContactDialog,
- icon: Icon(Icons.add),
- tooltip: i18n.addressBookAddContact,
- ),
- ],
- ),
+ appBar: AppBar(title: Text(i18n.addressBookTitle)),
bottomNavigationBar: WalletNavigationBar(selectedIndex: 1),
body: Column(
children: [
Padding(
padding: EdgeInsets.all(16.0),
- child: TextField(
- controller: _searchController,
- onChanged: _onSearchChanged,
- decoration: InputDecoration(
- hintText: i18n.addressBookSearchHint,
- prefixIcon: Icon(Icons.search),
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(8.0),
+ child: Row(
+ children: [
+ Expanded(
+ child: TextField(
+ controller: _searchController,
+ onChanged: _onSearchChanged,
+ decoration: InputDecoration(
+ hintText: i18n.addressBookSearchHint,
+ prefixIcon: Icon(Icons.search),
+ border: OutlineInputBorder(),
+ ),
+ ),
),
- ),
+ SizedBox(width: 8),
+ IconButton(onPressed: _showAddContactDialog, icon: Icon(Icons.add)),
+ ],
),
),
Expanded(
child: Consumer<ContactModel>(
builder: (context, contactModel, child) {
- final filteredContacts = contactModel.searchContacts(
- _searchQuery,
- );
+ final filteredContacts = contactModel.searchContacts(_searchQuery);
if (filteredContacts.isEmpty) {
return Center(
@@ -122,19 +118,17 @@ class _AddressBookScreenState extends State<AddressBookScreen> {
_searchQuery.isEmpty
? i18n.addressBookNoContacts
: i18n.addressBookNoSearchResults,
- style: Theme.of(context).textTheme.bodyLarge
- ?.copyWith(
- color: Theme.of(context).colorScheme.outline,
- ),
+ style: Theme.of(context).textTheme.bodyLarge?.copyWith(
+ color: Theme.of(context).colorScheme.outline,
+ ),
),
if (_searchQuery.isEmpty) ...[
SizedBox(height: 8),
Text(
i18n.addressBookNoContactsDescription,
- style: Theme.of(context).textTheme.bodyMedium
- ?.copyWith(
- color: Theme.of(context).colorScheme.outline,
- ),
+ style: Theme.of(context).textTheme.bodyMedium?.copyWith(
+ color: Theme.of(context).colorScheme.outline,
+ ),
textAlign: TextAlign.center,
),
],
@@ -168,20 +162,14 @@ class _ContactListItem extends StatelessWidget {
final VoidCallback onEdit;
final VoidCallback onDelete;
- const _ContactListItem({
- required this.contact,
- required this.onEdit,
- required this.onDelete,
- });
+ const _ContactListItem({required this.contact, required this.onEdit, required this.onDelete});
void _copyAddressToClipboard(BuildContext context) {
final i18n = AppLocalizations.of(context)!;
Clipboard.setData(ClipboardData(text: contact.address));
- ScaffoldMessenger.of(
- context,
- ).showSnackBar(SnackBar(content: Text(i18n.addressBookAddressCopied)));
+ ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(i18n.addressCopied)));
}
@override
@@ -201,10 +189,7 @@ class _ContactListItem extends StatelessWidget {
),
),
),
- title: Text(
- contact.name,
- style: TextStyle(fontWeight: FontWeight.w500),
- ),
+ title: Text(contact.name, style: TextStyle(fontWeight: FontWeight.w500)),
subtitle: Text(
contact.address,
style: TextStyle(fontFamily: 'monospace', fontSize: 12),
@@ -229,21 +214,13 @@ class _ContactListItem extends StatelessWidget {
PopupMenuItem(
value: 'copy',
child: Row(
- children: [
- Icon(Icons.copy),
- SizedBox(width: 8),
- Text(i18n.addressBookCopyAddress),
- ],
+ children: [Icon(Icons.copy), SizedBox(width: 8), Text(i18n.addressBookCopyAddress)],
),
),
PopupMenuItem(
value: 'edit',
child: Row(
- children: [
- Icon(Icons.edit),
- SizedBox(width: 8),
- Text(i18n.addressBookEdit),
- ],
+ children: [Icon(Icons.edit), SizedBox(width: 8), Text(i18n.addressBookEdit)],
),
),
PopupMenuItem(
@@ -252,10 +229,7 @@ class _ContactListItem extends StatelessWidget {
children: [
Icon(Icons.delete, color: Colors.red),
SizedBox(width: 8),
- Text(
- i18n.addressBookDelete,
- style: TextStyle(color: Colors.red),
- ),
+ Text(i18n.addressBookDelete, style: TextStyle(color: Colors.red)),
],
),
),
@@ -332,10 +306,7 @@ class _ContactDialogState extends State<_ContactDialog> {
final contactModel = Provider.of<ContactModel>(context, listen: false);
if (widget.contact == null) {
- await contactModel.addContact(
- _nameController.text.trim(),
- _addressController.text.trim(),
- );
+ await contactModel.addContact(_nameController.text.trim(), _addressController.text.trim());
} else {
await contactModel.updateContact(
widget.contact!.id,
@@ -349,12 +320,9 @@ class _ContactDialogState extends State<_ContactDialog> {
}
} catch (e) {
if (mounted) {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(
- content: Text(i18n.unknownError),
- backgroundColor: Colors.red,
- ),
- );
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(SnackBar(content: Text(i18n.unknownError), backgroundColor: Colors.red));
}
} finally {
if (mounted) {
@@ -371,10 +339,13 @@ class _ContactDialogState extends State<_ContactDialog> {
final isEditing = widget.contact != null;
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
+ final screenWidth = MediaQuery.of(context).size.width;
+ final dialogWidth = screenWidth.clamp(0.0, 400.0);
+
return AlertDialog(
- title: Text(
- isEditing ? i18n.addressBookEditContact : i18n.addressBookAddContact,
- ),
+ constraints: BoxConstraints.tightFor(width: dialogWidth),
+ insetPadding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 24.0),
+ title: Text(isEditing ? i18n.addressBookEditContact : i18n.addressBookAddContact),
content: Form(
key: _formKey,
child: Column(
@@ -384,9 +355,7 @@ class _ContactDialogState extends State<_ContactDialog> {
controller: _nameController,
decoration: InputDecoration(
labelText: i18n.addressBookContactName,
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(8.0),
- ),
+ border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
),
validator: _validateName,
textCapitalization: TextCapitalization.words,
@@ -396,9 +365,7 @@ class _ContactDialogState extends State<_ContactDialog> {
controller: _addressController,
decoration: InputDecoration(
labelText: i18n.address,
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(8.0),
- ),
+ border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
),
validator: _validateAddress,
maxLines: 3,
@@ -419,9 +386,7 @@ class _ContactDialogState extends State<_ContactDialog> {
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
- color: isDarkTheme
- ? Theme.of(context).colorScheme.onPrimary
- : Colors.white,
+ color: isDarkTheme ? Theme.of(context).colorScheme.onPrimary : Colors.white,
),
)
: Text(isEditing ? i18n.addressBookUpdate : i18n.addressBookSave),
diff --git a/lib/screens/confirm_send.dart b/lib/screens/confirm_send.dart
index 6f38374..3a42061 100644
--- a/lib/screens/confirm_send.dart
+++ b/lib/screens/confirm_send.dart
@@ -52,8 +52,7 @@ class _ConfirmSendScreenState extends State<ConfirmSendScreen> {
}
void _loadTxDetails() {
- final args =
- ModalRoute.of(context)!.settings.arguments as ConfirmSendScreenArgs?;
+ final args = ModalRoute.of(context)!.settings.arguments as ConfirmSendScreenArgs?;
if (args == null) {
throw Exception('Args missing');
@@ -75,10 +74,7 @@ class _ConfirmSendScreenState extends State<ConfirmSendScreen> {
for (int i = 0; i < address.length; i += 5) {
// Get the substring of the next 5 characters
- String substring = address.substring(
- i,
- i + 5 > address.length ? address.length : i + 5,
- );
+ String substring = address.substring(i, i + 5 > address.length ? address.length : i + 5);
result.add(substring);
}
@@ -103,31 +99,22 @@ class _ConfirmSendScreenState extends State<ConfirmSendScreen> {
});
if (mounted) {
- Navigator.pushNamed(
- context,
- '/wallet_home',
- arguments: {'showTxSuccessToast': true},
- );
+ Navigator.pushNamed(context, '/wallet_home', arguments: {'showTxSuccessToast': true});
}
} on FormatException catch (error) {
var errorMsg = error.toString().replaceFirst('FormatException: ', '');
if (error.toString().contains('HTTP error code 500')) {
- errorMsg =
- 'Failed to send transaction. You might have insufficient unlocked balance.';
+ errorMsg = 'Failed to send transaction. You might have insufficient unlocked balance.';
}
if (mounted) {
- ScaffoldMessenger.of(
- context,
- ).showSnackBar(SnackBar(content: Text(errorMsg)));
+ ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(errorMsg)));
}
} catch (error) {
log(LogLevel.error, error.toString());
if (mounted) {
- ScaffoldMessenger.of(
- context,
- ).showSnackBar(SnackBar(content: Text(i18n.unknownError)));
+ ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(i18n.unknownError)));
}
}
@@ -142,151 +129,136 @@ class _ConfirmSendScreenState extends State<ConfirmSendScreen> {
final isDarkTheme = Theme.of(context).brightness == Brightness.dark;
final fiatRate = context.watch<FiatRateModel>();
final fiatSymbol = fiatRate.fiatCode == 'EUR' ? '€' : '\$';
- final amountFiat = fiatRate.rate is double
- ? _amount * fiatRate.rate!
- : null;
- final networkFeeFiat = fiatRate.rate is double
- ? _fee * fiatRate.rate!
- : null;
+ final amountFiat = fiatRate.rate is double ? _amount * fiatRate.rate! : null;
+ final networkFeeFiat = fiatRate.rate is double ? _fee * fiatRate.rate! : null;
return Scaffold(
appBar: AppBar(),
body: Padding(
padding: EdgeInsets.symmetric(horizontal: 20),
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- spacing: 20,
- children: [
- Padding(
- padding: EdgeInsets.symmetric(horizontal: 20),
- child: Text(
- i18n.confirmSendTitle,
- style: Theme.of(context).textTheme.headlineMedium,
- ),
- ),
- Text(
- i18n.confirmSendDescription,
- textAlign: TextAlign.center,
- style: Theme.of(context).textTheme.bodyLarge,
- ),
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- crossAxisAlignment: CrossAxisAlignment.start,
+ child: Center(
+ child: Container(
+ constraints: BoxConstraints(maxWidth: 440),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ spacing: 20,
children: [
+ Padding(
+ padding: EdgeInsets.symmetric(horizontal: 20),
+ child: Text(
+ i18n.confirmSendTitle,
+ style: Theme.of(context).textTheme.headlineMedium,
+ ),
+ ),
Text(
- i18n.amount,
- style: TextStyle(fontWeight: FontWeight.bold),
+ i18n.confirmSendDescription,
+ textAlign: TextAlign.center,
+ style: Theme.of(context).textTheme.bodyLarge,
),
- Column(
- crossAxisAlignment: CrossAxisAlignment.end,
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Text(
- '${_amount.toStringAsFixed(12)} XMR',
- style: TextStyle(fontWeight: FontWeight.w500),
+ Text(i18n.amount, style: TextStyle(fontWeight: FontWeight.bold)),
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ Text(
+ '${_amount.toStringAsFixed(12)} XMR',
+ style: TextStyle(fontWeight: FontWeight.w500),
+ ),
+ if (amountFiat is double)
+ Text('$fiatSymbol${amountFiat.toStringAsFixed(2)}'),
+ ],
),
- if (amountFiat is double)
- Text('$fiatSymbol${amountFiat.toStringAsFixed(2)}'),
],
),
- ],
- ),
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- i18n.networkFee,
- style: TextStyle(fontWeight: FontWeight.bold),
- ),
- Column(
- crossAxisAlignment: CrossAxisAlignment.end,
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Text(
- '${_fee.toStringAsFixed(12)} XMR',
- style: TextStyle(fontWeight: FontWeight.w500),
+ Text(i18n.networkFee, style: TextStyle(fontWeight: FontWeight.bold)),
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ Text(
+ '${_fee.toStringAsFixed(12)} XMR',
+ style: TextStyle(fontWeight: FontWeight.w500),
+ ),
+ if (networkFeeFiat is double)
+ Text('$fiatSymbol${networkFeeFiat.toStringAsFixed(2)}'),
+ ],
),
- if (networkFeeFiat is double)
- Text('$fiatSymbol${networkFeeFiat.toStringAsFixed(2)}'),
],
),
- ],
- ),
- if (_destinationOpenAlias is String)
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- children: [
- Text(
- 'OpenAlias',
- style: TextStyle(fontWeight: FontWeight.bold),
- ),
- Text(_destinationOpenAlias!),
- ],
- ),
- Row(
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- i18n.address,
- style: TextStyle(fontWeight: FontWeight.bold),
- ),
-
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.end,
+ if (_destinationOpenAlias is String)
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- Container(
- margin: EdgeInsetsGeometry.only(left: 40),
- child: Wrap(
- spacing: 4,
- alignment: WrapAlignment.end,
- children: _destinationAddressSliced
- .asMap()
- .entries
- .map(
- (item) => Text(
- item.value,
- style: TextStyle(
- fontFamily: 'monospace',
- // highlight start and end slices of address
- fontWeight:
- item.key == 0 ||
- item.key ==
- _destinationAddressSliced
- .length -
- 1
- ? FontWeight.w700
- : FontWeight.w300,
- ),
- ),
- )
- .toList(),
- ),
- ),
- if (_destinationContactName is String)
- Text('($_destinationContactName)'),
+ Text('OpenAlias', style: TextStyle(fontWeight: FontWeight.bold)),
+ Text(_destinationOpenAlias!),
],
),
- ),
- ],
- ),
- FilledButton.icon(
- onPressed: _confirmSend,
- icon: !_isLoading
- ? Icon(Icons.arrow_outward_rounded)
- : SizedBox(
- width: 16,
- height: 16,
- child: CircularProgressIndicator(
- strokeWidth: 2,
- color: isDarkTheme
- ? Theme.of(context).colorScheme.onPrimary
- : Colors.white,
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(i18n.address, style: TextStyle(fontWeight: FontWeight.bold)),
+
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ Container(
+ margin: EdgeInsetsGeometry.only(left: 40),
+ child: Wrap(
+ spacing: 4,
+ alignment: WrapAlignment.end,
+ children: _destinationAddressSliced
+ .asMap()
+ .entries
+ .map(
+ (item) => Text(
+ item.value,
+ style: TextStyle(
+ fontFamily: 'monospace',
+ // highlight start and end slices of address
+ fontWeight:
+ item.key == 0 ||
+ item.key == _destinationAddressSliced.length - 1
+ ? FontWeight.w700
+ : FontWeight.w300,
+ ),
+ ),
+ )
+ .toList(),
+ ),
+ ),
+ if (_destinationContactName is String) Text('($_destinationContactName)'),
+ ],
),
),
- label: Text(i18n.sendSendButton),
+ ],
+ ),
+ FilledButton.icon(
+ onPressed: _confirmSend,
+ icon: !_isLoading
+ ? Icon(Icons.arrow_outward_rounded)
+ : SizedBox(
+ width: 16,
+ height: 16,
+ child: CircularProgressIndicator(
+ strokeWidth: 2,
+ color: isDarkTheme
+ ? Theme.of(context).colorScheme.onPrimary
+ : Colors.white,
+ ),
+ ),
+ label: Text(i18n.sendSendButton),
+ ),
+ ],
),
- ],
+ ),
),
),
);
diff --git a/lib/screens/receive.dart b/lib/screens/receive.dart
index 02a3ff5..d9f57b4 100644
--- a/lib/screens/receive.dart
+++ b/lib/screens/receive.dart
@@ -31,7 +31,9 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
@override
void dispose() {
- _setBrightnessToNormal();
+ if (Platform.isAndroid || Platform.isIOS) {
+ _setBrightnessToNormal();
+ }
super.dispose();
}
@@ -47,9 +49,15 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
}
Future<void> _setBrightnessToNormal() async {
- await ScreenBrightness().setApplicationScreenBrightness(
- _previousBrightness,
- );
+ await ScreenBrightness().setApplicationScreenBrightness(_previousBrightness);
+ }
+
+ void _copyAddressToClipboard(String address) {
+ final i18n = AppLocalizations.of(context)!;
+
+ Clipboard.setData(ClipboardData(text: address));
+
+ ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(i18n.addressCopied)));
}
@override
@@ -73,82 +81,85 @@ class _ReceiveScreenState extends State<ReceiveScreen> {
return Scaffold(
appBar: AppBar(title: Text(i18n.receiveTitle)),
body: Center(
- child: Padding(
- padding: EdgeInsets.symmetric(horizontal: 20),
- child: wallet.serverSupportsSubaddresses == null || address == null
- ? CircularProgressIndicator()
- : Column(
- mainAxisAlignment: MainAxisAlignment.center,
- spacing: 20,
- children: [
- QrImageView(
- data: address,
- eyeStyle: QrEyeStyle(
- eyeShape: QrEyeShape.square,
- color: isDarkTheme ? Colors.grey[300] : Colors.black,
- ),
- dataModuleStyle: QrDataModuleStyle(
- dataModuleShape: QrDataModuleShape.square,
- color: isDarkTheme ? Colors.grey[300] : Colors.black,
- ),
- ),
- if (wallet.serverSupportsSubaddresses == false)
- Text(
- i18n.receiveServerNoSubaddressesWarn,
- textAlign: TextAlign.center,
- style: TextStyle(color: Colors.red),
- ),
- if (!_showSubaddress)
- Text(
- i18n.receivePrimaryAddressWarn,
- textAlign: TextAlign.center,
- style: TextStyle(color: Colors.red),
+ child: Container(
+ constraints: BoxConstraints(maxWidth: 480),
+ child: Padding(
+ padding: EdgeInsets.symmetric(horizontal: 20),
+ child: wallet.serverSupportsSubaddresses == null || address == null
+ ? CircularProgressIndicator()
+ : Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ spacing: 20,
+ children: [
+ QrImageView(
+ data: address,
+ eyeStyle: QrEyeStyle(
+ eyeShape: QrEyeShape.square,
+ color: isDarkTheme ? Colors.grey[300] : Colors.black,
+ ),
+ dataModuleStyle: QrDataModuleStyle(
+ dataModuleShape: QrDataModuleShape.square,
+ color: isDarkTheme ? Colors.grey[300] : Colors.black,
+ ),
),
- if (_showSubaddress &&
- wallet.unusedSubaddressIndexIsSupported == false)
- Text(
- i18n.receiveMaxSubaddressesReachedWarn,
- textAlign: TextAlign.center,
- style: TextStyle(color: Colors.red),
+ if (wallet.serverSupportsSubaddresses == false)
+ Text(
+ i18n.receiveServerNoSubaddressesWarn,
+ textAlign: TextAlign.center,
+ style: TextStyle(color: Colors.red),
+ ),
+ if (!_showSubaddress)
+ Text(
+ i18n.receivePrimaryAddressWarn,
+ textAlign: TextAlign.center,
+ style: TextStyle(color: Colors.red),
+ ),
+ if (_showSubaddress && wallet.unusedSubaddressIndexIsSupported == false)
+ Text(
+ i18n.receiveMaxSubaddressesReachedWarn,
+ textAlign: TextAlign.center,
+ style: TextStyle(color: Colors.red),
+ ),
+ GestureDetector(
+ child: Text(
+ address,
+ textAlign: TextAlign.center,
+ style: TextStyle(fontFamily: 'monospace'),
+ ),
+ onTap: () => _copyAddressToClipboard(address!),
),
- GestureDetector(
- child: Text(
- address,
- textAlign: TextAlign.center,
- style: TextStyle(fontFamily: 'monospace'),
+ Row(
+ spacing: 20,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ if (Platform.isAndroid || Platform.isIOS)
+ FilledButton.icon(
+ onPressed: () => SharePlus.instance.share(ShareParams(text: address)),
+ icon: Icon(Icons.share),
+ label: Text(i18n.receiveShareButton),
+ ),
+ if (Platform.isLinux || Platform.isWindows || Platform.isMacOS)
+ FilledButton.icon(
+ onPressed: () => _copyAddressToClipboard(address!),
+ icon: Icon(Icons.copy),
+ label: Text(i18n.copy),
+ ),
+ // Only show toggle button if server supports subaddresses
+ if (wallet.serverSupportsSubaddresses == true && !_showSubaddress)
+ TextButton(
+ onPressed: () => _setShowSubaddress(true),
+ child: Text(i18n.receiveShowSubaddressButton),
+ ),
+ if (wallet.serverSupportsSubaddresses == true && _showSubaddress)
+ TextButton(
+ onPressed: () => _setShowSubaddress(false),
+ child: Text(i18n.receiveShowPrimaryAddressButton),
+ ),
+ ],
),
- onTap: () async {
- await Clipboard.setData(ClipboardData(text: address!));
- },
- ),
- Row(
- spacing: 20,
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- FilledButton.icon(
- onPressed: () => SharePlus.instance.share(
- ShareParams(text: address),
- ),
- icon: Icon(Icons.share),
- label: Text(i18n.receiveShareButton),
- ),
- // Only show toggle button if server supports subaddresses
- if (wallet.serverSupportsSubaddresses == true &&
- !_showSubaddress)
- TextButton(
- onPressed: () => _setShowSubaddress(true),
- child: Text(i18n.receiveShowSubaddressButton),
- ),
- if (wallet.serverSupportsSubaddresses == true &&
- _showSubaddress)
- TextButton(
- onPressed: () => _setShowSubaddress(false),
- child: Text(i18n.receiveShowPrimaryAddressButton),
- ),
- ],
- ),
- ],
- ),
+ ],
+ ),
+ ),
),
),
);
diff --git a/lib/screens/send.dart b/lib/screens/send.dart
index a2dc580..3303a18 100644
--- a/lib/screens/send.dart
+++ b/lib/screens/send.dart
@@ -67,9 +67,7 @@ class _SendScreenState extends State<SendScreen> {
if (args != null) {
_destinationAddressController.text = args.destinationAddress;
- _amountController.text = args.amount != null
- ? args.amount.toString()
- : '';
+ _amountController.text = args.amount != null ? args.amount.toString() : '';
}
}
@@ -110,9 +108,7 @@ class _SendScreenState extends State<SendScreen> {
String destinationAddress = '';
if (domainRegex.hasMatch(unresolvedDestinationAddress)) {
- destinationAddress = await wallet.resolveOpenAlias(
- unresolvedDestinationAddress,
- );
+ destinationAddress = await wallet.resolveOpenAlias(unresolvedDestinationAddress);
} else {
destinationAddress = unresolvedDestinationAddress;
}
@@ -134,9 +130,7 @@ class _SendScreenState extends State<SendScreen> {
if (domainRegex.hasMatch(unresolvedDestinationAddress)) {
// check for openalias
- destinationAddress = await wallet.resolveOpenAlias(
- unresolvedDestinationAddress,
- );
+ destinationAddress = await wallet.resolveOpenAlias(unresolvedDestinationAddress);
if (destinationAddress == '') {
if (setErrors) {
@@ -170,18 +164,9 @@ class _SendScreenState extends State<SendScreen> {
return true;
}
- Future<double> _getTxFee(
- String destinationAddress,
- double amount,
- int priority,
- ) async {
+ Future<double> _getTxFee(String destinationAddress, double amount, int priority) async {
final wallet = Provider.of<WalletModel>(context, listen: false);
- final tx = await wallet.createTx(
- destinationAddress,
- amount,
- _isSweepAll,
- priority: priority,
- );
+ final tx = await wallet.createTx(destinationAddress, amount, _isSweepAll, priority: priority);
return doubleAmountFromInt(tx.fee());
}
@@ -224,9 +209,9 @@ class _SendScreenState extends State<SendScreen> {
_isLoadingFees = false;
});
if (mounted) {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text(i18n.sendFailedToGetFeesError)),
- );
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(SnackBar(content: Text(i18n.sendFailedToGetFeesError)));
}
}
}
@@ -260,9 +245,7 @@ class _SendScreenState extends State<SendScreen> {
// Resolve openalias if it is a domain
if (domainRegex.hasMatch(destinationAddressUnresolved)) {
- destinationAddress = await wallet.resolveOpenAlias(
- destinationAddressUnresolved,
- );
+ destinationAddress = await wallet.resolveOpenAlias(destinationAddressUnresolved);
destinationOpenAlias = destinationAddressUnresolved;
} else {
destinationAddress = destinationAddressUnresolved;
@@ -305,9 +288,7 @@ class _SendScreenState extends State<SendScreen> {
}
} else {
if (mounted) {
- ScaffoldMessenger.of(
- context,
- ).showSnackBar(SnackBar(content: Text(i18n.unknownError)));
+ ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(i18n.unknownError)));
}
}
}
@@ -340,10 +321,7 @@ class _SendScreenState extends State<SendScreen> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Text(
- i18n.sendTransactionPriority,
- style: Theme.of(context).textTheme.titleLarge,
- ),
+ Text(i18n.sendTransactionPriority, style: Theme.of(context).textTheme.titleLarge),
SizedBox(height: 20),
_PriorityOption(
label: i18n.sendPriorityLow,
@@ -434,253 +412,221 @@ class _SendScreenState extends State<SendScreen> {
body: Center(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 20),
- child: Column(
- mainAxisAlignment: MainAxisAlignment.center,
- spacing: 20,
- children: [
- if (_selectedContact == null)
- TextField(
- controller: _destinationAddressController,
- maxLines: null,
- decoration: InputDecoration(
- labelText: i18n.address,
- border: OutlineInputBorder(),
- errorText: _destinationAddressError != ''
- ? _destinationAddressError
- : null,
- suffixIcon: Container(
- margin: EdgeInsets.only(right: 14),
- child: Row(
- spacing: 16,
- mainAxisSize: MainAxisSize.min,
- children: [
- GestureDetector(
- onTap: _pasteAddressFromClipboard,
- child: Icon(Icons.paste),
- ),
- if (Platform.isAndroid || Platform.isIOS)
+ child: Container(
+ constraints: BoxConstraints(maxWidth: 440),
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ spacing: 20,
+ children: [
+ if (_selectedContact == null)
+ TextField(
+ controller: _destinationAddressController,
+ maxLines: null,
+ decoration: InputDecoration(
+ labelText: i18n.address,
+ border: OutlineInputBorder(),
+ errorText: _destinationAddressError != '' ? _destinationAddressError : null,
+ suffixIcon: Container(
+ margin: EdgeInsets.only(right: 14),
+ child: Row(
+ spacing: 16,
+ mainAxisSize: MainAxisSize.min,
+ children: [
GestureDetector(
- onTap: () =>
- Navigator.pushNamed(context, '/scan_qr'),
- child: Icon(Icons.qr_code),
+ onTap: _pasteAddressFromClipboard,
+ child: Icon(Icons.paste),
),
- GestureDetector(
- onTap: _showContactPicker,
- child: Icon(Icons.contacts_outlined),
- ),
- ],
+ if (Platform.isAndroid || Platform.isIOS)
+ GestureDetector(
+ onTap: () => Navigator.pushNamed(context, '/scan_qr'),
+ child: Icon(Icons.qr_code),
+ ),
+ GestureDetector(
+ onTap: _showContactPicker,
+ child: Icon(Icons.contacts_outlined),
+ ),
+ ],
+ ),
),
),
),
- ),
- if (_selectedContact != null)
- Container(
- width: double.infinity,
- padding: EdgeInsets.all(12),
- decoration: BoxDecoration(
- color: Theme.of(context).colorScheme.primaryContainer,
- borderRadius: BorderRadius.circular(8),
- border: Border.all(
- color: Theme.of(
- context,
- ).colorScheme.outline.withValues(alpha: 0.2),
+ if (_selectedContact != null)
+ Container(
+ width: double.infinity,
+ padding: EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.primaryContainer,
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(
+ color: Theme.of(context).colorScheme.outline.withValues(alpha: 0.2),
+ ),
),
- ),
- child: Column(
- children: [
- Row(
- children: [
- CircleAvatar(
- radius: 16,
- backgroundColor: Theme.of(
- context,
- ).colorScheme.primary,
- child: Text(
- _selectedContact!.name.isNotEmpty
- ? _selectedContact!.name[0].toUpperCase()
- : '?',
- style: TextStyle(
- color: Theme.of(context).colorScheme.onPrimary,
- fontWeight: FontWeight.bold,
- fontSize: 14,
+ child: Column(
+ children: [
+ Row(
+ children: [
+ CircleAvatar(
+ radius: 16,
+ backgroundColor: Theme.of(context).colorScheme.primary,
+ child: Text(
+ _selectedContact!.name.isNotEmpty
+ ? _selectedContact!.name[0].toUpperCase()
+ : '?',
+ style: TextStyle(
+ color: Theme.of(context).colorScheme.onPrimary,
+ fontWeight: FontWeight.bold,
+ fontSize: 14,
+ ),
),
),
- ),
- SizedBox(width: 12),
- Expanded(
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- _selectedContact!.name,
- style: TextStyle(
- fontWeight: FontWeight.w500,
- fontSize: 16,
+ SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ _selectedContact!.name,
+ style: TextStyle(fontWeight: FontWeight.w500, fontSize: 16),
),
- ),
- Text(
- i18n.sendSelectedContact,
- style: TextStyle(
- fontSize: 12,
- color: Theme.of(
- context,
- ).colorScheme.onSurfaceVariant,
+ Text(
+ i18n.sendSelectedContact,
+ style: TextStyle(
+ fontSize: 12,
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
+ ),
),
- ),
- ],
+ ],
+ ),
),
- ),
- IconButton(
- onPressed: _clearSelectedContact,
- icon: Icon(Icons.close, size: 20),
- tooltip: i18n.sendClearSelectedContact,
- ),
- ],
- ),
- ],
- ),
- ),
- TextField(
- controller: _amountController,
- keyboardType: TextInputType.numberWithOptions(decimal: true),
- inputFormatters: [
- FilteringTextInputFormatter.allow(RegExp(r'^\d+(\.\d*)?')),
- ],
- decoration: InputDecoration(
- labelText: i18n.amount,
- border: OutlineInputBorder(),
- errorText: _amountError != '' ? _amountError : null,
- suffixIcon: TextButton(
- onPressed: _setBalanceAsSendAmount,
- child: Text('Max'),
- ),
- ),
- ),
- GestureDetector(
- onTap: _showPrioritySelector,
- child: Container(
- height: 40,
- padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
- decoration: BoxDecoration(
- color: Theme.of(
- context,
- ).colorScheme.surfaceContainerHighest,
- borderRadius: BorderRadius.circular(8),
- ),
- child: Row(
- children: [
- Icon(Icons.speed, size: 18),
- SizedBox(width: 8),
- Text(
- _selectedPriority == 0
- ? i18n.sendPriorityLow
- : _selectedPriority == 1
- ? i18n.sendPriorityNormal
- : i18n.sendPriorityHigh,
- style: TextStyle(fontSize: 14),
- ),
- Text(
- ' ${i18n.sendPriorityLabel}',
- style: TextStyle(
- fontSize: 14,
- color: Theme.of(context).colorScheme.onSurfaceVariant,
- ),
- ),
- Spacer(),
- if (_isLoadingFees)
- SizedBox(
- width: 16,
- height: 16,
- child: CircularProgressIndicator(strokeWidth: 2),
- )
- else if (_fees != null &&
- _fees!.length > _selectedPriority)
- Row(
- spacing: 8,
- children: [
- Row(
- crossAxisAlignment: CrossAxisAlignment.center,
- spacing: 4,
- children: [
- SvgPicture.asset(
- 'assets/icons/monero.svg',
- width: 14,
- height: 14,
- ),
- MoneroAmount(
- amount: _fees![_selectedPriority],
- maxFontSize: 14,
- ),
- ],
+ IconButton(
+ onPressed: _clearSelectedContact,
+ icon: Icon(Icons.close, size: 20),
+ tooltip: i18n.sendClearSelectedContact,
),
- Icon(Icons.arrow_drop_down),
],
- )
- else
- Icon(Icons.arrow_drop_down),
- ],
+ ),
+ ],
+ ),
+ ),
+ TextField(
+ controller: _amountController,
+ keyboardType: TextInputType.numberWithOptions(decimal: true),
+ inputFormatters: [FilteringTextInputFormatter.allow(RegExp(r'^\d+(\.\d*)?'))],
+ decoration: InputDecoration(
+ labelText: i18n.amount,
+ border: OutlineInputBorder(),
+ errorText: _amountError != '' ? _amountError : null,
+ suffixIcon: TextButton(onPressed: _setBalanceAsSendAmount, child: Text('Max')),
),
),
- ),
- Row(
- children: [
- Spacer(),
- GestureDetector(
- onTap: _setBalanceAsSendAmount,
+ GestureDetector(
+ onTap: _showPrioritySelector,
+ child: Container(
+ height: 40,
+ padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
+ decoration: BoxDecoration(
+ color: Theme.of(context).colorScheme.surfaceContainerHighest,
+ borderRadius: BorderRadius.circular(8),
+ ),
child: Row(
- spacing: 6,
children: [
+ Icon(Icons.speed, size: 18),
+ SizedBox(width: 8),
Text(
- '${i18n.sendBalanceLabel}:',
+ _selectedPriority == 0
+ ? i18n.sendPriorityLow
+ : _selectedPriority == 1
+ ? i18n.sendPriorityNormal
+ : i18n.sendPriorityHigh,
+ style: TextStyle(fontSize: 14),
+ ),
+ Text(
+ ' ${i18n.sendPriorityLabel}',
style: TextStyle(
fontSize: 14,
- color: Theme.of(
- context,
- ).colorScheme.onSurfaceVariant,
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
- SvgPicture.asset(
- 'assets/icons/monero.svg',
- width: 18,
- height: 18,
- ),
- MoneroAmount(
- amount: wallet.unlockedBalance ?? 0,
- maxFontSize: 18,
- ),
+ Spacer(),
+ if (_isLoadingFees)
+ SizedBox(
+ width: 16,
+ height: 16,
+ child: CircularProgressIndicator(strokeWidth: 2),
+ )
+ else if (_fees != null && _fees!.length > _selectedPriority)
+ Row(
+ spacing: 8,
+ children: [
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.center,
+ spacing: 4,
+ children: [
+ SvgPicture.asset(
+ 'assets/icons/monero.svg',
+ width: 14,
+ height: 14,
+ ),
+ MoneroAmount(amount: _fees![_selectedPriority], maxFontSize: 14),
+ ],
+ ),
+ Icon(Icons.arrow_drop_down),
+ ],
+ )
+ else
+ Icon(Icons.arrow_drop_down),
],
),
),
- ],
- ),
-
- Row(
- spacing: 20,
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- TextButton(
- onPressed: () => Navigator.pop(context),
- child: Text(i18n.cancel),
- ),
- FilledButton.icon(
- onPressed: _send,
- icon: !_isLoading
- ? Icon(Icons.arrow_outward_rounded)
- : SizedBox(
- width: 16,
- height: 16,
- child: CircularProgressIndicator(
- strokeWidth: 2,
- color: isDarkTheme
- ? Theme.of(context).colorScheme.onPrimary
- : Colors.white,
+ ),
+ Row(
+ children: [
+ Spacer(),
+ GestureDetector(
+ onTap: _setBalanceAsSendAmount,
+ child: Row(
+ spacing: 6,
+ children: [
+ Text(
+ '${i18n.sendBalanceLabel}:',
+ style: TextStyle(
+ fontSize: 14,
+ color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
- label: Text(i18n.sendSendButton),
- ),
- ],
- ),
- ],
+ SvgPicture.asset('assets/icons/monero.svg', width: 18, height: 18),
+ MoneroAmount(amount: wallet.unlockedBalance ?? 0, maxFontSize: 18),
+ ],
+ ),
+ ),
+ ],
+ ),
+
+ Row(
+ spacing: 20,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ TextButton(onPressed: () => Navigator.pop(context), child: Text(i18n.cancel)),
+ FilledButton.icon(
+ onPressed: _send,
+ icon: !_isLoading
+ ? Icon(Icons.arrow_outward_rounded)
+ : SizedBox(
+ width: 16,
+ height: 16,
+ child: CircularProgressIndicator(
+ strokeWidth: 2,
+ color: isDarkTheme
+ ? Theme.of(context).colorScheme.onPrimary
+ : Colors.white,
+ ),
+ ),
+ label: Text(i18n.sendSendButton),
+ ),
+ ],
+ ),
+ ],
+ ),
),
),
),
@@ -719,6 +665,7 @@ class _ContactPickerDialogState extends State<_ContactPickerDialog> {
return AlertDialog(
title: Text(i18n.sendSelectFromAddressBook),
+ insetPadding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 24.0),
content: SizedBox(
width: double.maxFinite,
height: 400,
@@ -730,18 +677,14 @@ class _ContactPickerDialogState extends State<_ContactPickerDialog> {
decoration: InputDecoration(
hintText: i18n.addressBookSearchHint,
prefixIcon: Icon(Icons.search),
- border: OutlineInputBorder(
- borderRadius: BorderRadius.circular(8.0),
- ),
+ border: OutlineInputBorder(borderRadius: BorderRadius.circular(8.0)),
),
),
SizedBox(height: 16),
Expanded(
child: Consumer<ContactModel>(
builder: (context, contactModel, child) {
- final filteredContacts = contactModel.searchContacts(
- _searchQuery,
- );
+ final filteredContacts = contactModel.searchContacts(_searchQuery);
if (filteredContacts.isEmpty) {
return Center(
@@ -762,29 +705,19 @@ class _ContactPickerDialogState extends State<_ContactPickerDialog> {
final contact = filteredContacts[index];
return ListTile(
leading: CircleAvatar(
- backgroundColor: Theme.of(
- context,
- ).colorScheme.primary,
+ backgroundColor: Theme.of(context).colorScheme.primary,
child: Text(
- contact.name.isNotEmpty
- ? contact.name[0].toUpperCase()
- : '?',
+ contact.name.isNotEmpty ? contact.name[0].toUpperCase() : '?',
style: TextStyle(
color: Theme.of(context).colorScheme.onPrimary,
fontWeight: FontWeight.bold,
),
),
),
- title: Text(
- contact.name,
- style: TextStyle(fontWeight: FontWeight.w500),
- ),
+ title: Text(contact.name, style: TextStyle(fontWeight: FontWeight.w500)),
subtitle: Text(
contact.address,
- style: TextStyle(
- fontFamily: 'monospace',
- fontSize: 12,
- ),
+ style: TextStyle(fontFamily: 'monospace', fontSize: 12),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
@@ -798,12 +731,7 @@ class _ContactPickerDialogState extends State<_ContactPickerDialog> {
],
),
),
- actions: [
- TextButton(
- onPressed: () => Navigator.of(context).pop(),
- child: Text(i18n.cancel),
- ),
- ],
+ actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: Text(i18n.cancel))],
);
}
}
@@ -845,17 +773,13 @@ class _PriorityOption extends StatelessWidget {
),
borderRadius: BorderRadius.circular(12),
color: isSelected
- ? Theme.of(
- context,
- ).colorScheme.primaryContainer.withValues(alpha: 0.3)
+ ? Theme.of(context).colorScheme.primaryContainer.withValues(alpha: 0.3)
: null,
),
child: Row(
children: [
Icon(
- isSelected
- ? Icons.radio_button_checked
- : Icons.radio_button_unchecked,
+ isSelected ? Icons.radio_button_checked : Icons.radio_button_unchecked,
color: isSelected
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurfaceVariant,
@@ -884,20 +808,12 @@ class _PriorityOption extends StatelessWidget {
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
- SvgPicture.asset(
- 'assets/icons/monero.svg',
- width: 14,
- height: 14,
- ),
+ SvgPicture.asset('assets/icons/monero.svg', width: 14, height: 14),
MoneroAmount(amount: fee!, maxFontSize: 14),
],
),
if (fiatRate is double)
- FiatAmount(
- prefix: fiatSymbol,
- amount: fee! * fiatRate!,
- maxFontSize: 12,
- ),
+ FiatAmount(prefix: fiatSymbol, amount: fee! * fiatRate!, maxFontSize: 12),
],
),
],
diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart
index b2acc68..5e80d89 100644
--- a/lib/screens/settings.dart
+++ b/lib/screens/settings.dart
@@ -37,27 +37,17 @@ class _SettingsScreenState extends State<SettingsScreen> {
void _loadPreferences() async {
final newTxNotificationsEnabled =
- await SharedPreferencesService.get<bool>(
- SharedPreferencesKeys.notificationsEnabled,
- ) ??
+ await SharedPreferencesService.get<bool>(SharedPreferencesKeys.notificationsEnabled) ??
false;
final fiatCurrency =
- await SharedPreferencesService.get<String>(
- SharedPreferencesKeys.fiatCurrency,
- ) ??
- 'USD';
+ await SharedPreferencesService.get<String>(SharedPreferencesKeys.fiatCurrency) ?? 'USD';
final appLockEnabled =
- await SharedPreferencesService.get<bool>(
- SharedPreferencesKeys.appLockEnabled,
- ) ??
- false;
+ await SharedPreferencesService.get<bool>(SharedPreferencesKeys.appLockEnabled) ?? false;
final verboseLoggingEnabled =
- await SharedPreferencesService.get<bool>(
- SharedPreferencesKeys.verboseLoggingEnabled,
- ) ??
+ await SharedPreferencesService.get<bool>(SharedPreferencesKeys.verboseLoggingEnabled) ??
false;
setState(() {
@@ -77,17 +67,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
final isAllowed = await NotificationService().promptPermission();
if (isAllowed) {
- await SharedPreferencesService.set<bool>(
- SharedPreferencesKeys.notificationsEnabled,
- true,
- );
+ await SharedPreferencesService.set<bool>(SharedPreferencesKeys.notificationsEnabled, true);
await registerTxNotifierTaskIfAllowed();
}
} else {
- await SharedPreferencesService.set<bool>(
- SharedPreferencesKeys.notificationsEnabled,
- false,
- );
+ await SharedPreferencesService.set<bool>(SharedPreferencesKeys.notificationsEnabled, false);
await unregisterPeriodicTasks();
}
}
@@ -101,17 +85,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
try {
final didAuthenticate = await auth.authenticate(
localizedReason: i18n.settingsAppLockUnlockReason,
- options: AuthenticationOptions(
- useErrorDialogs: true,
- sensitiveTransaction: true,
- ),
+ options: AuthenticationOptions(useErrorDialogs: true, sensitiveTransaction: true),
);
if (!didAuthenticate) {
if (mounted) {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text(i18n.settingsAppLockUnableToAuthError)),
- );
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(SnackBar(content: Text(i18n.settingsAppLockUnableToAuthError)));
}
return;
}
@@ -119,9 +100,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
log(LogLevel.error, 'Unable to authenticate: ${error.toString()}');
if (mounted) {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text(i18n.settingsAppLockUnableToAuthError)),
- );
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(SnackBar(content: Text(i18n.settingsAppLockUnableToAuthError)));
}
return;
}
@@ -131,10 +112,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
_appLockEnabled = value;
});
- await SharedPreferencesService.set<bool>(
- SharedPreferencesKeys.appLockEnabled,
- value,
- );
+ await SharedPreferencesService.set<bool>(SharedPreferencesKeys.appLockEnabled, value);
}
void _setVerboseLoggingEnabled(bool value) async {
@@ -142,10 +120,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
_verboseLoggingEnabled = value;
});
- await SharedPreferencesService.set<bool>(
- SharedPreferencesKeys.verboseLoggingEnabled,
- value,
- );
+ await SharedPreferencesService.set<bool>(SharedPreferencesKeys.verboseLoggingEnabled, value);
}
void _setFiatCurrency(String? value) async {
@@ -155,10 +130,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
_fiatCurrency = value;
});
- await SharedPreferencesService.set<String>(
- SharedPreferencesKeys.fiatCurrency,
- value,
- );
+ await SharedPreferencesService.set<String>(SharedPreferencesKeys.fiatCurrency, value);
// Clear rate
await SharedPreferencesService.remove(SharedPreferencesKeys.fiatRate);
@@ -182,6 +154,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
Text(i18n.settingsDeleteWalletButton),
],
),
+ insetPadding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 24.0),
content: Text(i18n.settingsDeleteWalletDialogText),
actions: [
TextButton.icon(
@@ -215,6 +188,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
Text(i18n.warning),
],
),
+ insetPadding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 24.0),
content: Text(i18n.settingsViewLwsKeysDialogText),
actions: [
TextButton.icon(
@@ -251,6 +225,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
Text(i18n.warning),
],
),
+ insetPadding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 24.0),
content: Text(i18n.settingsViewSecretKeysDialogText),
actions: [
TextButton.icon(
@@ -278,11 +253,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
final wallet = Provider.of<WalletModel>(context, listen: false);
await wallet.delete();
if (mounted) {
- Navigator.pushNamedAndRemoveUntil(
- context,
- '/welcome',
- (Route<dynamic> route) => false,
- );
+ Navigator.pushNamedAndRemoveUntil(context, '/welcome', (Route<dynamic> route) => false);
}
}
@@ -312,14 +283,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
value: 'system',
child: Text(i18n.settingsThemeSystem),
),
- DropdownMenuItem<String>(
- value: 'light',
- child: Text(i18n.settingsThemeLight),
- ),
- DropdownMenuItem<String>(
- value: 'dark',
- child: Text(i18n.settingsThemeDark),
- ),
+ DropdownMenuItem<String>(value: 'light', child: Text(i18n.settingsThemeLight)),
+ DropdownMenuItem<String>(value: 'dark', child: Text(i18n.settingsThemeDark)),
],
),
],
@@ -327,10 +292,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- Text(
- i18n.settingsLanguageLabel,
- style: TextStyle(fontSize: 18),
- ),
+ Text(i18n.settingsLanguageLabel, style: TextStyle(fontSize: 18)),
DropdownButton<String>(
value: language.language,
onChanged: language.setLanguage,
@@ -346,18 +308,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- Text(
- i18n.settingsDisplayCurrencyLabel,
- style: TextStyle(fontSize: 18),
- ),
+ Text(i18n.settingsDisplayCurrencyLabel, style: TextStyle(fontSize: 18)),
DropdownButton<String>(
value: _fiatCurrency,
onChanged: _setFiatCurrency,
items: supportedFiatCurrencies.map((fiatCode) {
- return DropdownMenuItem<String>(
- value: fiatCode,
- child: Text(fiatCode),
- );
+ return DropdownMenuItem<String>(value: fiatCode, child: Text(fiatCode));
}).toList(),
),
],
@@ -373,14 +329,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- Text(
- i18n.settingsNotifyNewTxsLabel,
- style: TextStyle(fontSize: 18),
- ),
- Switch(
- value: _newTxNotificationsEnabled,
- onChanged: _setTxNotificationsEnabled,
- ),
+ Text(i18n.settingsNotifyNewTxsLabel, style: TextStyle(fontSize: 18)),
+ Switch(value: _newTxNotificationsEnabled, onChanged: _setTxNotificationsEnabled),
],
),
Row(
@@ -390,10 +340,7 @@ class _SettingsScreenState extends State<SettingsScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Text(
- i18n.settingsVerboseLoggingLabel,
- style: TextStyle(fontSize: 18),
- ),
+ Text(i18n.settingsVerboseLoggingLabel, style: TextStyle(fontSize: 18)),
Text(
i18n.settingsVerboseLoggingDescription,
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
@@ -401,66 +348,44 @@ class _SettingsScreenState extends State<SettingsScreen> {
],
),
),
- Switch(
- value: _verboseLoggingEnabled,
- onChanged: _setVerboseLoggingEnabled,
- ),
+ Switch(value: _verboseLoggingEnabled, onChanged: _setVerboseLoggingEnabled),
],
),
- Container(
- margin: EdgeInsetsGeometry.symmetric(vertical: 10),
- child: Divider(),
- ),
+ Container(margin: EdgeInsetsGeometry.symmetric(vertical: 10), child: Divider()),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- Text(
- i18n.settingsLwsViewKeysLabel,
- style: TextStyle(fontSize: 18),
- ),
+ Text(i18n.settingsLwsViewKeysLabel, style: TextStyle(fontSize: 18)),
TextButton.icon(
onPressed: _showViewLwsKeysDialog,
icon: Icon(Icons.key),
label: Text(i18n.settingsLwsViewKeysButton),
- style: ButtonStyle(
- foregroundColor: WidgetStateProperty.all(Colors.orange),
- ),
+ style: ButtonStyle(foregroundColor: WidgetStateProperty.all(Colors.orange)),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
- Text(
- i18n.settingsSecretKeysLabel,
- style: TextStyle(fontSize: 18),
- ),
+ Text(i18n.settingsSecretKeysLabel, style: TextStyle(fontSize: 18)),
TextButton.icon(
onPressed: _showViewSecretKeysDialog,
icon: Icon(Icons.key),
label: Text(i18n.settingsSecretKeysButton),
- style: ButtonStyle(
- foregroundColor: WidgetStateProperty.all(Colors.red),
- ),
+ style: ButtonStyle(foregroundColor: WidgetStateProperty.all(Colors.red)),
),
],
),
- Container(
- margin: EdgeInsetsGeometry.symmetric(vertical: 10),
- child: Divider(),
- ),
+ Container(margin: EdgeInsetsGeometry.symmetric(vertical: 10), child: Divider()),
TextButton.icon(
onPressed: _showDeleteWalletDialog,
label: Text(i18n.settingsDeleteWalletButton),
icon: Icon(Icons.delete),
- style: ButtonStyle(
- foregroundColor: WidgetStateProperty.all(Colors.red),
- ),
+ style: ButtonStyle(foregroundColor: WidgetStateProperty.all(Colors.red)),
),
Spacer(),
TextButton(
- onPressed: () =>
- Navigator.pushNamed(context, '/terms_of_service'),
+ onPressed: () => Navigator.pushNamed(context, '/terms_of_service'),
child: Text('Terms of Service'),
),
TextButton(
diff --git a/lib/screens/tx_details.dart b/lib/screens/tx_details.dart
deleted file mode 100644
index 13c5311..0000000
--- a/lib/screens/tx_details.dart
+++ /dev/null
@@ -1,158 +0,0 @@
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
-import 'package:intl/intl.dart';
-import 'package:skylight_wallet/l10n/app_localizations.dart';
-import 'package:skylight_wallet/models/wallet_model.dart';
-
-class TxDetailsScreen extends StatelessWidget {
- const TxDetailsScreen({super.key});
-
- @override
- Widget build(BuildContext context) {
- final i18n = AppLocalizations.of(context)!;
- final locale = Localizations.localeOf(context);
- final txDetails = ModalRoute.of(context)!.settings.arguments as TxDetails;
- final amountSent = txDetails.amount.toString();
- final fee = txDetails.fee.toString();
-
- final dateTime = DateTime.fromMillisecondsSinceEpoch(
- txDetails.timestamp * 1000,
- );
-
- final dateFormatted = DateFormat.yMMMMd(locale.toString()).format(dateTime);
- final timeFormatted = DateFormat.jm(locale.toString()).format(dateTime);
-
- return Scaffold(
- appBar: AppBar(title: Text(i18n.txDetailsTitle)),
- body: SafeArea(
- child: Padding(
- padding: EdgeInsets.symmetric(horizontal: 20),
- child: Column(
- spacing: 10,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- i18n.txDetailsHashLabel,
- style: TextStyle(fontWeight: FontWeight.bold),
- ),
- GestureDetector(
- child: Text(
- txDetails.hash,
- style: TextStyle(fontFamily: 'monospace'),
- ),
- onTap: () =>
- Clipboard.setData(ClipboardData(text: txDetails.hash)),
- ),
- ],
- ),
- Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- i18n.amount,
- style: TextStyle(fontWeight: FontWeight.bold),
- ),
- Text('$amountSent XMR'),
- ],
- ),
- Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- i18n.networkFee,
- style: TextStyle(fontWeight: FontWeight.bold),
- ),
- Text('$fee XMR'),
- ],
- ),
- Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- i18n.txDetailsTimeAndDateLabel,
- style: TextStyle(fontWeight: FontWeight.bold),
- ),
- Text('$dateFormatted $timeFormatted'),
- ],
- ),
- Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- i18n.txDetailsConfirmationHeightLabel,
- style: TextStyle(fontWeight: FontWeight.bold),
- ),
- Text(txDetails.height.toString()),
- ],
- ),
- Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- i18n.txDetailsConfirmationsLabel,
- style: TextStyle(fontWeight: FontWeight.bold),
- ),
- Text(txDetails.confirmations.toString()),
- ],
- ),
- Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- i18n.txDetailsViewKeyLabel,
- style: TextStyle(fontWeight: FontWeight.bold),
- ),
- GestureDetector(
- child: Text(
- txDetails.key,
- style: TextStyle(fontFamily: 'monospace'),
- ),
- onTap: () =>
- Clipboard.setData(ClipboardData(text: txDetails.key)),
- ),
- ],
- ),
- Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- Text(
- i18n.txDetailsRecipientsLabel,
- style: TextStyle(fontWeight: FontWeight.bold),
- ),
- ListView.separated(
- shrinkWrap: true,
- itemCount: txDetails.recipients.length,
- separatorBuilder: (context, index) => SizedBox(height: 10),
- itemBuilder: (context, index) {
- final recipient = txDetails.recipients[index];
- final amountStr = recipient.amount.toString();
-
- return Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- GestureDetector(
- child: Text(
- recipient.address,
- style: TextStyle(fontFamily: 'monospace'),
- ),
- onTap: () => Clipboard.setData(
- ClipboardData(text: recipient.address),
- ),
- ),
- Text('$amountStr XMR'),
- ],
- );
- },
- ),
- ],
- ),
- ],
- ),
- ),
- ),
- );
- }
-}
diff --git a/lib/screens/wallet_home.dart b/lib/screens/wallet_home.dart
index be03db5..386f061 100644
--- a/lib/screens/wallet_home.dart
+++ b/lib/screens/wallet_home.dart
@@ -13,9 +13,124 @@ import 'package:skylight_wallet/l10n/app_localizations.dart';
import 'package:skylight_wallet/models/wallet_model.dart';
import 'package:skylight_wallet/consts.dart' as consts;
import 'package:skylight_wallet/widgets/wallet_navigation_bar.dart';
+import 'package:skylight_wallet/widgets/tx_details.dart';
enum LwsConnectionStatus { disconnected, connecting, connected }
+enum DeviceType { phone, tablet, desktop }
+
+class _TransactionListItem extends StatefulWidget {
+ final TxDetails tx;
+ final AppLocalizations i18n;
+ final Locale currentLocale;
+ final FiatRateModel fiatRate;
+ final String fiatSymbol;
+ final VoidCallback onTap;
+
+ const _TransactionListItem({
+ required this.tx,
+ required this.i18n,
+ required this.currentLocale,
+ required this.fiatRate,
+ required this.fiatSymbol,
+ required this.onTap,
+ });
+
+ @override
+ State<_TransactionListItem> createState() => _TransactionListItemState();
+}
+
+class _TransactionListItemState extends State<_TransactionListItem> {
+ bool _isHovered = false;
+
+ @override
+ Widget build(BuildContext context) {
+ final amountFiat = widget.fiatRate.rate is double
+ ? widget.tx.amount * widget.fiatRate.rate!
+ : null;
+
+ return MouseRegion(
+ cursor: SystemMouseCursors.click,
+ onEnter: (_) => setState(() => _isHovered = true),
+ onExit: (_) => setState(() => _isHovered = false),
+ child: Container(
+ height: 64,
+ decoration: BoxDecoration(
+ color: _isHovered
+ ? Theme.of(context).colorScheme.surfaceContainerHighest
+ : Colors.transparent,
+ ),
+ child: GestureDetector(
+ onTap: widget.onTap,
+ child: Padding(
+ padding: EdgeInsetsDirectional.symmetric(vertical: 10, horizontal: 20),
+ child: Row(
+ spacing: 20,
+ children: [
+ if (widget.tx.direction == consts.txDirectionOutgoing)
+ Icon(
+ Icons.arrow_outward_rounded,
+ color: Colors.red,
+ size: 20,
+ semanticLabel: widget.i18n.homeOutgoingTxSemanticLabel,
+ ),
+ if (widget.tx.direction == consts.txDirectionIncoming)
+ Transform.rotate(
+ angle: 90 * math.pi / 180,
+ child: Icon(
+ Icons.arrow_outward_rounded,
+ color: Colors.teal,
+ size: 20,
+ semanticLabel: widget.i18n.homeIncomingTxSemanticLabel,
+ ),
+ ),
+ Column(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ MoneroAmount(amount: widget.tx.amount, maxFontSize: 16),
+ if (amountFiat == null)
+ Skeletonizer(
+ enabled: true,
+ child: Text('Potato', style: TextStyle(fontSize: 14)),
+ ),
+ if (amountFiat is double)
+ FiatAmount(prefix: widget.fiatSymbol, amount: amountFiat, maxFontSize: 14),
+ ],
+ ),
+ Spacer(),
+ Column(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ crossAxisAlignment: CrossAxisAlignment.end,
+ children: [
+ if (widget.tx.confirmations < 10)
+ Row(
+ children: [
+ Text(
+ '${widget.tx.confirmations}/10',
+ style: TextStyle(color: Colors.amber.shade700),
+ ),
+ Icon(Icons.hourglass_top_rounded, color: Colors.amber.shade700, size: 20),
+ ],
+ ),
+ if (widget.tx.confirmations >= 10) Text(''),
+ Text(
+ timeago.format(
+ DateTime.fromMillisecondsSinceEpoch(widget.tx.timestamp * 1000),
+ locale: widget.currentLocale.languageCode,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
class WalletHomeScreen extends StatefulWidget {
const WalletHomeScreen({super.key});
@@ -24,6 +139,21 @@ class WalletHomeScreen extends StatefulWidget {
}
class _WalletHomeScreenState extends State<WalletHomeScreen> {
+ // Breakpoints for responsive design
+ static const double _phoneMaxWidth = 700;
+ static const double _tabletMaxWidth = 1024;
+
+ DeviceType _getDeviceType(BuildContext context) {
+ final width = MediaQuery.of(context).size.width;
+ if (width < _phoneMaxWidth) {
+ return DeviceType.phone;
+ } else if (width < _tabletMaxWidth) {
+ return DeviceType.tablet;
+ } else {
+ return DeviceType.desktop;
+ }
+ }
+
@override
void initState() {
super.initState();
@@ -49,14 +179,151 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
}
void _showTxDetails(TxDetails txDetails) {
- Navigator.pushNamed(context, '/tx_details', arguments: txDetails);
+ TxDetailsDialog.show(context, txDetails);
}
void _showTxSuccessToast() {
final i18n = AppLocalizations.of(context)!;
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text(i18n.sendTransactionSuccessfullySent)),
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(SnackBar(content: Text(i18n.sendTransactionSuccessfullySent)));
+ }
+
+ Widget _buildStatusIcons(
+ WalletModel wallet,
+ StatusIconStatus lwsConnectionIconStatus,
+ StatusIconStatus fiatApiIconStatus,
+ ) {
+ return Column(
+ spacing: 10,
+ children: [
+ if (!wallet.usingTor && lwsConnectionIconStatus == StatusIconStatus.complete)
+ SizedBox(
+ width: 26,
+ height: 26,
+ child: Icon(Icons.check_circle, size: 26, color: Colors.teal),
+ ),
+ if (!wallet.usingTor && lwsConnectionIconStatus == StatusIconStatus.loading)
+ SizedBox(
+ width: 22,
+ height: 22,
+ child: CircularProgressIndicator(
+ constraints: BoxConstraints(maxWidth: 22, maxHeight: 22),
+ strokeWidth: 2,
+ ),
+ ),
+ if (!wallet.usingTor && lwsConnectionIconStatus == StatusIconStatus.fail)
+ SizedBox(width: 26, height: 26, child: Icon(Icons.cancel, color: Colors.red)),
+ if (wallet.usingTor)
+ StatusIcon(
+ status: lwsConnectionIconStatus,
+ child: SvgPicture.asset('assets/icons/tor.svg', width: 22, height: 22),
+ ),
+ StatusIcon(
+ status: fiatApiIconStatus,
+ child: SvgPicture.asset('assets/icons/tor.svg', width: 22, height: 22),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildBalanceDisplay(
+ BuildContext context,
+ AppLocalizations i18n,
+ WalletModel wallet,
+ double? unlockedBalanceFiat,
+ double lockedBalance,
+ String fiatSymbol,
+ ) {
+ return Column(
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ spacing: 10,
+ children: [
+ SvgPicture.asset('assets/icons/monero.svg', width: 22, height: 22),
+ MoneroAmount(amount: wallet.unlockedBalance ?? 0, maxFontSize: 30),
+ ],
+ ),
+ if (lockedBalance > 0)
+ Text(
+ '+${lockedBalance.toStringAsFixed(12)} ${i18n.pending.toLowerCase()}',
+ textAlign: TextAlign.center,
+ style: Theme.of(context).textTheme.bodySmall,
+ ),
+ if (unlockedBalanceFiat == null)
+ Skeletonizer(enabled: true, child: Text('Potato', style: TextStyle(fontSize: 18))),
+ if (unlockedBalanceFiat is double)
+ FiatAmount(prefix: fiatSymbol, amount: unlockedBalanceFiat, maxFontSize: 18),
+ ],
+ );
+ }
+
+ Widget _buildActionButtons(AppLocalizations i18n) {
+ return Row(
+ spacing: 10,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ FilledButton.icon(
+ style: ButtonStyle(),
+ label: Text(i18n.homeReceive),
+ icon: Transform.rotate(
+ angle: 90 * math.pi / 180,
+ child: Icon(Icons.arrow_outward_rounded),
+ ),
+ onPressed: () => Navigator.pushNamed(context, '/receive'),
+ ),
+ FilledButton.icon(
+ label: Text(i18n.homeSend),
+ icon: Icon(Icons.arrow_outward_rounded),
+ onPressed: () => Navigator.pushNamed(context, '/send'),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildTransactionListItem(
+ BuildContext context,
+ AppLocalizations i18n,
+ Locale currentLocale,
+ TxDetails tx,
+ FiatRateModel fiatRate,
+ String fiatSymbol,
+ ) {
+ return _TransactionListItem(
+ tx: tx,
+ i18n: i18n,
+ currentLocale: currentLocale,
+ fiatRate: fiatRate,
+ fiatSymbol: fiatSymbol,
+ onTap: () => _showTxDetails(tx),
+ );
+ }
+
+ Widget _buildTransactionList(
+ BuildContext context,
+ AppLocalizations i18n,
+ Locale currentLocale,
+ WalletModel wallet,
+ FiatRateModel fiatRate,
+ String fiatSymbol,
+ ) {
+ if (wallet.txHistory.isEmpty) {
+ return Text(i18n.homeNoTransactions);
+ }
+
+ return Expanded(
+ child: ListView.separated(
+ separatorBuilder: (context, index) =>
+ Container(height: 1, color: Theme.of(context).colorScheme.surfaceContainerHighest),
+ itemCount: wallet.txHistory.length,
+ itemBuilder: (BuildContext context, int index) {
+ final tx = wallet.txHistory[index];
+ return _buildTransactionListItem(context, i18n, currentLocale, tx, fiatRate, fiatSymbol);
+ },
+ ),
);
}
@@ -66,28 +333,22 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
final currentLocale = Localizations.localeOf(context);
final wallet = context.watch<WalletModel>();
final fiatRate = context.watch<FiatRateModel>();
- final unlockedBalanceFiat =
- fiatRate.rate is double && wallet.unlockedBalance is double
+ final deviceType = _getDeviceType(context);
+ final unlockedBalanceFiat = fiatRate.rate is double && wallet.unlockedBalance is double
? wallet.unlockedBalance! * fiatRate.rate!
: null;
- final lockedBalance =
- (wallet.totalBalance ?? 0) - (wallet.unlockedBalance ?? 0);
+ final lockedBalance = (wallet.totalBalance ?? 0) - (wallet.unlockedBalance ?? 0);
final fiatSymbol = consts.currencySymbols[fiatRate.fiatCode] ?? '\$';
var lwsConnectionIconStatus = StatusIconStatus.fail;
var fiatApiIconStatus = StatusIconStatus.loading;
- if (wallet.isConnected &&
- wallet.isSynced &&
- (wallet.syncedHeight ?? 0) > 0) {
+ if (wallet.isConnected && wallet.isSynced && (wallet.syncedHeight ?? 0) > 0) {
lwsConnectionIconStatus = StatusIconStatus.complete;
} else if (wallet.usingTor &&
- TorService.sharedInstance.status ==
- TorConnectionStatus.connecting ||
+ TorService.sharedInstance.status == TorConnectionStatus.connecting ||
!wallet.hasAttemptedConnection ||
wallet.isConnected && !wallet.isSynced ||
- wallet.isConnected &&
- wallet.isSynced &&
- (wallet.syncedHeight ?? 0) == 0) {
+ wallet.isConnected && wallet.isSynced && (wallet.syncedHeight ?? 0) == 0) {
lwsConnectionIconStatus = StatusIconStatus.loading;
}
@@ -102,275 +363,233 @@ class _WalletHomeScreenState extends State<WalletHomeScreen> {
return Scaffold(
bottomNavigationBar: WalletNavigationBar(selectedIndex: 0),
body: SafeArea(
- child: Column(
- spacing: 20,
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- Padding(
- padding: EdgeInsetsGeometry.all(20),
- child: Column(
- spacing: 20,
- children: [
- SizedBox(
- width: double.infinity,
- height: 86,
- child: Stack(
- children: [
- Positioned(
- top: 0,
- right: 0,
- child: Column(
- spacing: 10,
- // TODO: this can be made better
- children: [
- if (!wallet.usingTor &&
- lwsConnectionIconStatus ==
- StatusIconStatus.complete)
- SizedBox(
- width: 26,
- height: 26,
- child: Icon(
- Icons.check_circle,
- size: 26,
- color: Colors.teal,
- ),
- ),
- if (!wallet.usingTor &&
- lwsConnectionIconStatus ==
- StatusIconStatus.loading)
- SizedBox(
- width: 22,
- height: 22,
- child: CircularProgressIndicator(
- constraints: BoxConstraints(
- maxWidth: 22,
- maxHeight: 22,
- ),
- strokeWidth: 2,
- ),
- ),
- if (!wallet.usingTor &&
- lwsConnectionIconStatus ==
- StatusIconStatus.fail)
- SizedBox(
- width: 26,
- height: 26,
- child: Icon(Icons.cancel, color: Colors.red),
- ),
- if (wallet.usingTor)
- StatusIcon(
- status: lwsConnectionIconStatus,
- child: SvgPicture.asset(
- 'assets/icons/tor.svg',
- width: 22,
- height: 22,
- ),
- ),
- StatusIcon(
- status: fiatApiIconStatus,
- child: SvgPicture.asset(
- 'assets/icons/tor.svg',
- width: 22,
- height: 22,
- ),
- ),
- ],
- ),
- ),
- Center(
- child: Column(
- children: [
- Row(
- mainAxisAlignment: MainAxisAlignment.center,
- crossAxisAlignment: CrossAxisAlignment.center,
- spacing: 10,
- children: [
- SvgPicture.asset(
- 'assets/icons/monero.svg',
- width: 22,
- height: 22,
- ),
- MoneroAmount(
- amount: wallet.unlockedBalance ?? 0,
- maxFontSize: 30,
- ),
- ],
- ),
- if (lockedBalance > 0)
- Text(
- '+${lockedBalance.toStringAsFixed(12)} ${i18n.pending.toLowerCase()}',
- textAlign: TextAlign.center,
- style: Theme.of(context).textTheme.bodySmall,
- ),
- if (unlockedBalanceFiat == null)
- Skeletonizer(
- enabled: true,
- child: Text(
- 'Potato',
- style: TextStyle(fontSize: 18),
- ),
- ),
- if (unlockedBalanceFiat is double)
- FiatAmount(
- prefix: fiatSymbol,
- amount: unlockedBalanceFiat,
- maxFontSize: 18,
- ),
- ],
- ),
- ),
- ],
+ child: _buildResponsiveLayout(
+ context,
+ deviceType,
+ i18n,
+ currentLocale,
+ wallet,
+ fiatRate,
+ lwsConnectionIconStatus,
+ fiatApiIconStatus,
+ unlockedBalanceFiat,
+ lockedBalance,
+ fiatSymbol,
+ ),
+ ),
+ );
+ }
+
+ Widget _buildResponsiveLayout(
+ BuildContext context,
+ DeviceType deviceType,
+ AppLocalizations i18n,
+ Locale currentLocale,
+ WalletModel wallet,
+ FiatRateModel fiatRate,
+ StatusIconStatus lwsConnectionIconStatus,
+ StatusIconStatus fiatApiIconStatus,
+ double? unlockedBalanceFiat,
+ double lockedBalance,
+ String fiatSymbol,
+ ) {
+ switch (deviceType) {
+ case DeviceType.phone:
+ return _buildPhoneLayout(
+ context,
+ i18n,
+ currentLocale,
+ wallet,
+ fiatRate,
+ lwsConnectionIconStatus,
+ fiatApiIconStatus,
+ unlockedBalanceFiat,
+ lockedBalance,
+ fiatSymbol,
+ );
+ case DeviceType.tablet:
+ return _buildTabletLayout(
+ context,
+ i18n,
+ currentLocale,
+ wallet,
+ fiatRate,
+ lwsConnectionIconStatus,
+ fiatApiIconStatus,
+ unlockedBalanceFiat,
+ lockedBalance,
+ fiatSymbol,
+ );
+ case DeviceType.desktop:
+ return _buildDesktopLayout(
+ context,
+ i18n,
+ currentLocale,
+ wallet,
+ fiatRate,
+ lwsConnectionIconStatus,
+ fiatApiIconStatus,
+ unlockedBalanceFiat,
+ lockedBalance,
+ fiatSymbol,
+ );
+ }
+ }
+
+ Widget _buildPhoneLayout(
+ BuildContext context,
+ AppLocalizations i18n,
+ Locale currentLocale,
+ WalletModel wallet,
+ FiatRateModel fiatRate,
+ StatusIconStatus lwsConnectionIconStatus,
+ StatusIconStatus fiatApiIconStatus,
+ double? unlockedBalanceFiat,
+ double lockedBalance,
+ String fiatSymbol,
+ ) {
+ return Column(
+ spacing: 20,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ Padding(
+ padding: EdgeInsetsGeometry.all(20),
+ child: Column(
+ spacing: 20,
+ children: [
+ SizedBox(
+ width: double.infinity,
+ height: 86,
+ child: Stack(
+ children: [
+ Positioned(
+ top: 0,
+ right: 0,
+ child: _buildStatusIcons(wallet, lwsConnectionIconStatus, fiatApiIconStatus),
),
- ),
- Row(
- spacing: 10,
- mainAxisAlignment: MainAxisAlignment.center,
- children: [
- FilledButton.icon(
- style: ButtonStyle(),
- label: Text(i18n.homeReceive),
- icon: Transform.rotate(
- angle: 90 * math.pi / 180,
- child: Icon(Icons.arrow_outward_rounded),
- ),
- onPressed: () =>
- Navigator.pushNamed(context, '/receive'),
- ),
- FilledButton.icon(
- label: Text(i18n.homeSend),
- icon: Icon(Icons.arrow_outward_rounded),
- onPressed: () => Navigator.pushNamed(context, '/send'),
+ Center(
+ child: _buildBalanceDisplay(
+ context,
+ i18n,
+ wallet,
+ unlockedBalanceFiat,
+ lockedBalance,
+ fiatSymbol,
),
- ],
- ),
- ],
+ ),
+ ],
+ ),
),
+ _buildActionButtons(i18n),
+ ],
+ ),
+ ),
+ Align(
+ alignment: Alignment.centerLeft,
+ child: Padding(
+ padding: EdgeInsetsGeometry.symmetric(horizontal: 20),
+ child: Text(
+ i18n.homeTransactionsTitle,
+ style: Theme.of(context).textTheme.titleLarge,
+ textAlign: TextAlign.start,
),
- Align(
- alignment: Alignment.centerLeft,
- child: Padding(
- padding: EdgeInsetsGeometry.symmetric(horizontal: 20),
- child: Text(
- i18n.homeTransactionsTitle,
- style: Theme.of(context).textTheme.titleLarge,
- textAlign: TextAlign.start,
+ ),
+ ),
+ _buildTransactionList(context, i18n, currentLocale, wallet, fiatRate, fiatSymbol),
+ ],
+ );
+ }
+
+ Widget _buildTabletLayout(
+ BuildContext context,
+ AppLocalizations i18n,
+ Locale currentLocale,
+ WalletModel wallet,
+ FiatRateModel fiatRate,
+ StatusIconStatus lwsConnectionIconStatus,
+ StatusIconStatus fiatApiIconStatus,
+ double? unlockedBalanceFiat,
+ double lockedBalance,
+ String fiatSymbol,
+ ) {
+ return Padding(
+ padding: EdgeInsets.all(20),
+ child: Row(
+ spacing: 20,
+ children: [
+ SizedBox(
+ width: 340,
+ child: Stack(
+ children: [
+ Positioned(
+ top: 0,
+ left: 0,
+ child: _buildStatusIcons(wallet, lwsConnectionIconStatus, fiatApiIconStatus),
),
- ),
+ Column(
+ children: [
+ _buildBalanceDisplay(
+ context,
+ i18n,
+ wallet,
+ unlockedBalanceFiat,
+ lockedBalance,
+ fiatSymbol,
+ ),
+ SizedBox(height: 10),
+ _buildActionButtons(i18n),
+ ],
+ ),
+ ],
),
- if (wallet.txHistory.isNotEmpty)
- Expanded(
- child: SizedBox(
- child: ListView.separated(
- separatorBuilder: (context, index) => Divider(),
- itemCount: wallet.txHistory.length,
- itemBuilder: (BuildContext context, int index) {
- final tx = wallet.txHistory[index];
- final amountFiat = fiatRate.rate is double
- ? tx.amount * fiatRate.rate!
- : null;
-
- return Padding(
- padding: EdgeInsetsDirectional.symmetric(
- horizontal: 20,
- vertical: 4,
- ),
- child: SizedBox(
- height: 44,
- child: GestureDetector(
- onTap: () => _showTxDetails(tx),
- child: Row(
- spacing: 20,
- children: [
- if (tx.direction == consts.txDirectionOutgoing)
- Icon(
- Icons.arrow_outward_rounded,
- color: Colors.red,
- size: 20,
- semanticLabel:
- i18n.homeOutgoingTxSemanticLabel,
- ),
- if (tx.direction == consts.txDirectionIncoming)
- Transform.rotate(
- angle: 90 * math.pi / 180,
- child: Icon(
- Icons.arrow_outward_rounded,
- color: Colors.teal,
- size: 20,
- semanticLabel:
- i18n.homeIncomingTxSemanticLabel,
- ),
- ),
- Column(
- mainAxisAlignment:
- MainAxisAlignment.spaceBetween,
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- MoneroAmount(
- amount: tx.amount,
- maxFontSize: 16,
- ),
- if (amountFiat == null)
- Skeletonizer(
- enabled: true,
- child: Text(
- 'Potato',
- style: TextStyle(fontSize: 14),
- ),
- ),
- if (amountFiat is double)
- FiatAmount(
- prefix: fiatSymbol,
- amount: amountFiat,
- maxFontSize: 14,
- ),
- ],
- ),
- Spacer(),
- Column(
- mainAxisAlignment:
- MainAxisAlignment.spaceBetween,
- crossAxisAlignment: CrossAxisAlignment.end,
- children: [
- if (tx.confirmations < 10)
- Row(
- children: [
- Text(
- '${tx.confirmations}/10',
- style: TextStyle(
- color: Colors.amber.shade700,
- ),
- ),
- Icon(
- Icons.hourglass_top_rounded,
- color: Colors.amber.shade700,
- size: 20,
- ),
- ],
- ),
- if (tx.confirmations >= 10) Text(''),
- Text(
- timeago.format(
- DateTime.fromMillisecondsSinceEpoch(
- tx.timestamp * 1000,
- ),
- locale: currentLocale.languageCode,
- ),
- ),
- ],
- ),
- ],
- ),
- ),
- ),
- );
- },
+ ),
+ Expanded(
+ child: Column(
+ spacing: 20,
+ children: [
+ Align(
+ alignment: Alignment.centerLeft,
+ child: Padding(
+ padding: EdgeInsetsGeometry.symmetric(horizontal: 20),
+ child: Text(
+ i18n.homeTransactionsTitle,
+ style: Theme.of(context).textTheme.titleLarge,
+ textAlign: TextAlign.start,
+ ),
),
),
- ),
- if (wallet.txHistory.isEmpty) Text(i18n.homeNoTransactions),
- ],
- ),
+ _buildTransactionList(context, i18n, currentLocale, wallet, fiatRate, fiatSymbol),
+ ],
+ ),
+ ),
+ ],
),
);
}
+
+ Widget _buildDesktopLayout(
+ BuildContext context,
+ AppLocalizations i18n,
+ Locale currentLocale,
+ WalletModel wallet,
+ FiatRateModel fiatRate,
+ StatusIconStatus lwsConnectionIconStatus,
+ StatusIconStatus fiatApiIconStatus,
+ double? unlockedBalanceFiat,
+ double lockedBalance,
+ String fiatSymbol,
+ ) {
+ return _buildTabletLayout(
+ context,
+ i18n,
+ currentLocale,
+ wallet,
+ fiatRate,
+ lwsConnectionIconStatus,
+ fiatApiIconStatus,
+ unlockedBalanceFiat,
+ lockedBalance,
+ fiatSymbol,
+ );
+ }
}
diff --git a/lib/widgets/tx_details.dart b/lib/widgets/tx_details.dart
new file mode 100644
index 0000000..f481bea
--- /dev/null
+++ b/lib/widgets/tx_details.dart
@@ -0,0 +1,205 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:intl/intl.dart';
+import 'package:skylight_wallet/l10n/app_localizations.dart';
+import 'package:skylight_wallet/models/wallet_model.dart';
+
+class TxDetailsDialog {
+ static void show(BuildContext context, TxDetails txDetails) {
+ showDialog(
+ context: context,
+ builder: (context) => _TxDetailsDialog(txDetails: txDetails),
+ );
+ }
+}
+
+class _TxDetailsDialog extends StatelessWidget {
+ final TxDetails txDetails;
+
+ const _TxDetailsDialog({required this.txDetails});
+
+ @override
+ Widget build(BuildContext context) {
+ final i18n = AppLocalizations.of(context)!;
+ final locale = Localizations.localeOf(context);
+ final amountSent = txDetails.amount.toString();
+ final fee = txDetails.fee.toString();
+
+ final dateTime = DateTime.fromMillisecondsSinceEpoch(txDetails.timestamp * 1000);
+
+ final dateFormatted = DateFormat.yMMMMd(locale.toString()).format(dateTime);
+ final timeFormatted = DateFormat.jm(locale.toString()).format(dateTime);
+
+ final screenWidth = MediaQuery.of(context).size.width;
+ final dialogWidth = screenWidth.clamp(0.0, 500.0);
+
+ return AlertDialog(
+ constraints: BoxConstraints.tightFor(width: dialogWidth),
+ insetPadding: EdgeInsets.symmetric(horizontal: 16.0, vertical: 24.0),
+ title: Text(i18n.txDetailsTitle),
+ content: SingleChildScrollView(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ spacing: 10,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ spacing: 20,
+ children: [
+ Text(i18n.txDetailsHashLabel, style: TextStyle(fontWeight: FontWeight.bold)),
+ Flexible(
+ child: Container(
+ constraints: BoxConstraints(maxWidth: 240),
+ child: GestureDetector(
+ child: Text(
+ txDetails.hash,
+ textAlign: TextAlign.end,
+ style: TextStyle(fontFamily: 'monospace'),
+ softWrap: true,
+ ),
+ onTap: () => Clipboard.setData(ClipboardData(text: txDetails.hash)),
+ ),
+ ),
+ ),
+ ],
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ spacing: 20,
+ children: [
+ Text(i18n.amount, style: TextStyle(fontWeight: FontWeight.bold)),
+ Flexible(child: Text('$amountSent XMR', textAlign: TextAlign.end, softWrap: true)),
+ ],
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ spacing: 20,
+ children: [
+ Text(i18n.networkFee, style: TextStyle(fontWeight: FontWeight.bold)),
+ Flexible(child: Text('$fee XMR', textAlign: TextAlign.end, softWrap: true)),
+ ],
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ spacing: 20,
+ children: [
+ Text(i18n.txDetailsTimeAndDateLabel, style: TextStyle(fontWeight: FontWeight.bold)),
+ Flexible(
+ child: Text(
+ '$dateFormatted $timeFormatted',
+ textAlign: TextAlign.end,
+ softWrap: true,
+ ),
+ ),
+ ],
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ spacing: 20,
+ children: [
+ Text(
+ i18n.txDetailsConfirmationHeightLabel,
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ Flexible(
+ child: Text(
+ txDetails.height.toString(),
+ textAlign: TextAlign.end,
+ softWrap: true,
+ ),
+ ),
+ ],
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ spacing: 20,
+ children: [
+ Text(
+ i18n.txDetailsConfirmationsLabel,
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ Flexible(
+ child: Text(
+ txDetails.confirmations.toString(),
+ textAlign: TextAlign.end,
+ softWrap: true,
+ ),
+ ),
+ ],
+ ),
+ if (txDetails.key.isNotEmpty)
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ spacing: 20,
+ children: [
+ Text(i18n.txDetailsViewKeyLabel, style: TextStyle(fontWeight: FontWeight.bold)),
+ Flexible(
+ child: Container(
+ constraints: BoxConstraints(maxWidth: 240),
+ child: GestureDetector(
+ child: Text(
+ txDetails.key,
+ textAlign: TextAlign.end,
+ style: TextStyle(fontFamily: 'monospace'),
+ softWrap: true,
+ ),
+ onTap: () => Clipboard.setData(ClipboardData(text: txDetails.key)),
+ ),
+ ),
+ ),
+ ],
+ ),
+ if (txDetails.recipients.isNotEmpty)
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ crossAxisAlignment: CrossAxisAlignment.start,
+ spacing: 20,
+ children: [
+ Text(
+ i18n.txDetailsRecipientsLabel,
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ Expanded(
+ child: ListView.separated(
+ shrinkWrap: true,
+ itemCount: txDetails.recipients.length,
+ separatorBuilder: (context, index) => SizedBox(height: 10),
+ itemBuilder: (context, index) {
+ final recipient = txDetails.recipients[index];
+ final amountStr = recipient.amount.toString();
+
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ GestureDetector(
+ child: Text(
+ recipient.address,
+ style: TextStyle(fontFamily: 'monospace'),
+ softWrap: true,
+ ),
+ onTap: () =>
+ Clipboard.setData(ClipboardData(text: recipient.address)),
+ ),
+ Text('$amountStr XMR', softWrap: true),
+ ],
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ actions: [TextButton(onPressed: () => Navigator.of(context).pop(), child: Text(i18n.close))],
+ );
+ }
+}
Why this scored 18/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.