Skip to content
Snippets Groups Projects
ui_helpers.dart 24 KiB
Newer Older
vjrj's avatar
vjrj committed
import 'dart:io';

vjrj's avatar
vjrj committed
import 'package:clipboard/clipboard.dart';
vjrj's avatar
vjrj committed
import 'package:easy_localization/easy_localization.dart';
import 'package:fast_image_resizer/fast_image_resizer.dart';
vjrj's avatar
vjrj committed
import 'package:flutter/foundation.dart';
vjrj's avatar
vjrj committed
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:get_it/get_it.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
vjrj's avatar
vjrj committed
import 'package:path_provider/path_provider.dart';
vjrj's avatar
vjrj committed
import 'package:qr_flutter/qr_flutter.dart';
vjrj's avatar
vjrj committed
import 'package:timeago/timeago.dart' as timeago;
import 'package:url_launcher/url_launcher.dart';
vjrj's avatar
vjrj committed

import '../data/models/app_cubit.dart';
import '../data/models/cesium_card.dart';
import '../data/models/contact.dart';
vjrj's avatar
vjrj committed
import '../data/models/multi_wallet_transaction_cubit.dart';
import '../data/models/node_list_cubit.dart';
vjrj's avatar
vjrj committed
import '../data/models/utxo_cubit.dart';
import '../g1/api.dart';
vjrj's avatar
vjrj committed
import '../g1/currency.dart';
import '../g1/g1_helper.dart';
import '../shared_prefs_helper.dart';
vjrj's avatar
vjrj committed
import 'logger.dart';
import 'notification_controller.dart';
vjrj's avatar
vjrj committed
import 'widgets/first_screen/circular_icon.dart';

Future<dynamic> showAlertDialog(
    BuildContext context, String title, String message) {
  return showDialog(
vjrj's avatar
vjrj committed
    context: context,
    builder: (BuildContext context) {
      return AlertDialog(
        title: Text(title),
        content: Text(message),
        actions: <Widget>[
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: Text(
              tr('close').toUpperCase(),
            ),
          ),
        ],
      );
    },
  );
}
vjrj's avatar
vjrj committed

vjrj's avatar
vjrj committed
void copyPublicKeyToClipboard(BuildContext context,
    [String? uri, String? feedbackText]) {
vjrj's avatar
vjrj committed
  FlutterClipboard.copy(uri ?? SharedPreferencesHelper().getPubKey()).then(
vjrj's avatar
vjrj committed
      (dynamic value) => ScaffoldMessenger.of(context).showSnackBar(SnackBar(
          content: Text(tr(feedbackText ?? 'key_copied_to_clipboard')))));
vjrj's avatar
vjrj committed
}

vjrj's avatar
vjrj committed
void copyToClipboard(
    {required BuildContext context,
    required String uri,
    required String feedbackText}) {
  FlutterClipboard.copy(uri).then((dynamic value) =>
      ScaffoldMessenger.of(context)
          .showSnackBar(SnackBar(content: Text(tr(feedbackText)))));
}

vjrj's avatar
vjrj committed
const Color defAvatarBgColor = Colors.grey;
const Color defAvatarColor = Colors.white;
const double defAvatarStoreSize = 44;
const double defAvatarUiSize = 24;
vjrj's avatar
vjrj committed

vjrj's avatar
vjrj committed
Widget avatar(Uint8List? rawAvatar,
    {Color color = defAvatarColor,
    Color bgColor = defAvatarBgColor,
    double avatarSize = defAvatarUiSize}) {
vjrj's avatar
vjrj committed
  return rawAvatar != null && rawAvatar.isNotEmpty
vjrj's avatar
vjrj committed
      ? CircleAvatar(
vjrj's avatar
vjrj committed
          child: ClipOval(
              child: Image.memory(
vjrj's avatar
vjrj committed
            rawAvatar,
vjrj's avatar
vjrj committed
            fit: BoxFit.cover,
          )))
      : CircularIcon(
          iconData: Icons.person, backgroundColor: color, iconColor: bgColor);
}
vjrj's avatar
vjrj committed

String humanizeFromToPubKey(String publicAddress, String address) {
  if (address == publicAddress) {
    return tr('your_wallet');
  } else {
    return humanizePubKey(address);
  }
}

vjrj's avatar
vjrj committed
String humanizeContacts(
vjrj's avatar
vjrj committed
    {required String publicAddress, required List<Contact> contacts}) {
vjrj's avatar
vjrj committed
  if (contacts.length > 3) {
vjrj's avatar
vjrj committed
    return '${contacts.take(3).map((Contact contact) => humanizeContact(publicAddress, contact)).join(', ')}...';
vjrj's avatar
vjrj committed
  } else if (contacts.length > 1) {
vjrj's avatar
vjrj committed
    final String lastContact = humanizeContact(publicAddress, contacts.last);
    final String otherContacts = contacts
        .take(contacts.length - 1)
        .map((Contact contact) => humanizeContact(publicAddress, contact))
vjrj's avatar
vjrj committed
        .join(', ');
    return tr('others_and_someone', namedArgs: <String, String>{
      'others': otherContacts,
      'someone': lastContact,
    });
  } else {
vjrj's avatar
vjrj committed
    return contacts
        .map((Contact contact) => humanizeContact(publicAddress, contact))
vjrj's avatar
vjrj committed
        .join(', ');
  }
}

String humanizeContact(String publicAddress, Contact contact,
    [bool addKey = false,
    bool minimal = false,
    String Function(String s) trf = tr]) {
  if (isMe(contact, publicAddress)) {
    final String pubKey = humanizePubKey(contact.pubKey);
    final String pubKeyMin = humanizePubKey(contact.pubKey, minimal);
    final bool titleNotTheSameAsPubKey = contact.title != pubKey;
    return addKey && titleNotTheSameAsPubKey
        ? minimal
            ? '${contact.title} $pubKeyMin'
            : '${contact.title} ($pubKey)'
        : titleNotTheSameAsPubKey
            ? contact.title
            : pubKey;
bool isMe(Contact contact, String publicAddress) =>
    extractPublicKey(contact.pubKey) == extractPublicKey(publicAddress);

String humanizePubKey(String rawAddress, [bool minimal = false]) {
  final String address = extractPublicKey(rawAddress);
  return minimal
      ? '\u{1F511} ${simplifyPubKey(address).substring(0, 4)}'
      : '\u{1F511} ${simplifyPubKey(address)}';
}
vjrj's avatar
vjrj committed

String simplifyPubKey(String address) => address.length <= 8
    ? 'WRONG ADDRESS'
    : '${address.substring(0, 4)}${address.substring(address.length - 4)}';
vjrj's avatar
vjrj committed
Color tileColor(int index, BuildContext context, [bool inverse = false]) {
  final ColorScheme colorScheme = Theme.of(context).colorScheme;
  final Color selectedColor = colorScheme.primary.withOpacity(0.1);
  final Color unselectedColor = colorScheme.surface;
  return (inverse ? index.isOdd : index.isEven)
      ? selectedColor
      : unselectedColor;
}
vjrj's avatar
vjrj committed

// https://github.com/andresaraujo/timeago.dart/pull/142#issuecomment-859661123
vjrj's avatar
vjrj committed
String? humanizeTime(DateTime time, String locale) =>
    timeago.format(time.toUtc(), locale: locale, clock: DateTime.now().toUtc());
vjrj's avatar
vjrj committed
const bool txDebugging = false;

vjrj's avatar
vjrj committed
const int smallScreenWidth = 360;
vjrj's avatar
vjrj committed

bool bigScreen(BuildContext context) =>
    MediaQuery.of(context).size.width > smallScreenWidth;

bool smallScreen(BuildContext context) =>
vjrj's avatar
vjrj committed
    MediaQuery.of(context).size.width <= smallScreenWidth;
vjrj's avatar
vjrj committed

vjrj's avatar
vjrj committed
String _formatAmount(
    {required String locale,
    required double amount,
    required bool isG1,
    required bool useSymbol}) {
vjrj's avatar
vjrj committed
  return formatAmountWithLocale(
      locale: locale, amount: amount, isG1: isG1, useSymbol: useSymbol);
String formatAmountWithLocale(
    {required String locale,
    required double amount,
    required bool isG1,
    required bool useSymbol}) {
  final NumberFormat currencyFormatter =
      currentNumberFormat(isG1: isG1, locale: locale, useSymbol: useSymbol);
  return currencyFormatter.format(amount);
}

NumberFormat currentNumberFormat(
    {required bool useSymbol, required bool isG1, required String locale}) {
vjrj's avatar
vjrj committed
  final NumberFormat currencyFormatter = NumberFormat.currency(
    symbol: useSymbol ? currentCurrency(isG1) : '',
    locale: eo(locale),
vjrj's avatar
vjrj committed
    decimalDigits: isG1 ? 2 : 3,
vjrj's avatar
vjrj committed
  );
  return currencyFormatter;
}

String currentCurrency(bool isG1) {
  return isG1 ? '${Currency.G1.name()} ' : '${Currency.DU.name()} ';
}

String currentCurrencyTrimmed(bool isG1) {
  return currentCurrency(isG1).trim();
vjrj's avatar
vjrj committed
}

vjrj's avatar
vjrj committed
String formatKAmountInView(
        {required BuildContext context,
        required double amount,
        required bool isG1,
        required double currentUd,
        required bool useSymbol}) =>
vjrj's avatar
vjrj committed
    _formatAmount(
        locale: currentLocale(context),
        amount: convertAmount(isG1, amount, currentUd),
        isG1: isG1,
        useSymbol: useSymbol);

String formatKAmountInViewWithLocale(
        {required String locale,
        required double amount,
        required bool isG1,
        required double currentUd,
        required bool useSymbol}) =>
    _formatAmount(
        locale: locale,
        amount: convertAmount(isG1, amount, currentUd),
        isG1: isG1,
        useSymbol: useSymbol);
vjrj's avatar
vjrj committed

double convertAmount(bool isG1, double amount, double currentUd) =>
    isG1 ? amount / 100 : ((amount / 100) / currentUd);

// NumberFormat does not work with esperanto nowadays, so we use
// this fallback
// https://en.wikipedia.org/wiki/Decimal_separator
// The three most spoken international auxiliary languages, Ido, Esperanto, and
// Interlingua, all use the comma as the decimal separator.
String eo(String locale) => locale == 'eo' ? 'es' : locale;

vjrj's avatar
vjrj committed
double parseToDoubleLocalized(
        {required String locale, required String number}) =>
    NumberFormat.decimalPattern(eo(locale)).parse(number).toDouble();
vjrj's avatar
vjrj committed
String localizeNumber(BuildContext context, double amount) =>
    NumberFormat.decimalPattern(eo(currentLocale(context))).format(amount);
vjrj's avatar
vjrj committed

Future<Contact> contactFromResultSearch(Map<String, dynamic> record,
    {bool resize = true}) async {
  final Map<String, dynamic> source = record['_source'] as Map<String, dynamic>;
  final Uint8List? avatarBase64 = await _getAvatarFromResults(source, resize);
  return Contact(
      pubKey: record['_id'] as String,
      name: source['title'] as String,
      avatar: avatarBase64);
}

Future<Uint8List?> _getAvatarFromResults(
    Map<String, dynamic> source, bool resize) async {
  Uint8List? avatarBase64;
  if (source['avatar'] != null) {
    final Map<String, dynamic> avatar =
        source['avatar'] as Map<String, dynamic>;
    avatarBase64 = imageFromBase64String(
        'data:${avatar['_content_type']};base64,${avatar['_content']}');
  }
  if (avatarBase64 != null && avatarBase64.isNotEmpty) {
    final Uint8List? avatarBase64resized =
        resize ? await resizeAvatar(avatarBase64) : avatarBase64;
    return avatarBase64resized;
  } else {
    return null;
  }
}

Future<Uint8List?> resizeAvatar(Uint8List avatarBase64) async {
  final ByteData? bytes =
      await resizeImage(avatarBase64, height: defAvatarStoreSize.toInt() * 2);
  return bytes != null ? Uint8List.view(bytes.buffer) : null;

final RegExp basicEnglishCharsRegExp =
    RegExp(r'^[ A-Za-z0-9\s.;:!?()\-_;!@&<>%]*$');
final RegExp basicEnglishCharsRegExpNegative =
    RegExp(r'[^ A-Za-z0-9\s.;:!?()\-_;!@&<>%]');

String cleanComment(String? comment) {
  return comment == null
      ? ''
      : comment.replaceAllMapped(
          basicEnglishCharsRegExpNegative, (Match match) => ' ');
}
void initGetItAll() {
  final GetIt getIt = GetIt.instance;
  if (!getIt.isRegistered<MultiWalletTransactionCubit>()) {
    getIt.registerSingleton<MultiWalletTransactionCubit>(
        MultiWalletTransactionCubit());
    getIt.registerSingleton<AppCubit>(AppCubit());
    getIt.registerSingleton<NodeListCubit>(NodeListCubit());
vjrj's avatar
vjrj committed
    getIt.registerSingleton<UtxoCubit>(UtxoCubit());
vjrj's avatar
vjrj committed
Future<void> fetchTransactionsFromBackground([bool init = true]) async {
  try {
    if (init) {
      await hydratedInit();
      if (SharedPreferencesHelper().cards.isEmpty) {
        await SharedPreferencesHelper().init();
      }
      try {
        initGetItAll();
        await NotificationController.initializeLocalNotifications();
      } catch (e) {
        // We should try to do this better
        loggerDev(e.toString());
        if (inDevelopment) {
          NotificationController.notify(
              title: 'Background process failed',
              desc: e.toString(),
              id: DateTime.now().toIso8601String());
        }
    loggerDev('Initialized background context');
    final GetIt getIt = GetIt.instance;
    final AppCubit appCubit = getIt.get<AppCubit>();
    final MultiWalletTransactionCubit transCubit =
        getIt.get<MultiWalletTransactionCubit>();
    final NodeListCubit nodeListCubit = getIt.get<NodeListCubit>();
    for (final CesiumCard card in SharedPreferencesHelper().cards) {
      loggerDev('Fetching transactions for ${card.pubKey} in background');
vjrj's avatar
vjrj committed
      transCubit.fetchTransactions(nodeListCubit, appCubit,
          pubKey: card.pubKey);
    }
    if (inDevelopment) {
      NotificationController.notify(
          title: 'Background process ended correctly',
          desc: '',
          id: DateTime.now().toIso8601String());
    }
  } catch (e) {
    // We should try to do this better
    loggerDev(e.toString());
}

Future<void> fetchTransactions(BuildContext context) async {
vjrj's avatar
vjrj committed
  final AppCubit appCubit = context.read<AppCubit>();
vjrj's avatar
vjrj committed
  final MultiWalletTransactionCubit transCubit =
      context.read<MultiWalletTransactionCubit>();
  final NodeListCubit nodeListCubit = context.read<NodeListCubit>();
  for (final CesiumCard card in SharedPreferencesHelper().cards) {
vjrj's avatar
vjrj committed
    transCubit.fetchTransactions(nodeListCubit, appCubit, pubKey: card.pubKey);
vjrj's avatar
vjrj committed

class SlidableContactTile extends StatefulWidget {
  const SlidableContactTile(this.contact,
      {super.key,
      required this.index,
      required this.context,
      this.onTap,
      this.onLongPress,
      this.trailing});

  @override
  State<SlidableContactTile> createState() => _SlidableContactTile();

  final Contact contact;
  final int index;
  final BuildContext context;
  final VoidCallback? onTap;
  final VoidCallback? onLongPress;
  final Widget? trailing;
}

class _SlidableContactTile extends State<SlidableContactTile> {
  @override
  void initState() {
    super.initState();
    // Disable for now
    //   _start();
  }

  // Based in https://github.com/letsar/flutter_slidable/issues/288
  Future<void> _start() async {
    if (widget.index == 0 &&
        !context.read<AppCubit>().wasTutorialShown(tutorialId)) {
      await Future<void>.delayed(const Duration(seconds: 1));
      if (!mounted) {
        return;
      }
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text(tr('slidable_tutorial')),
          action: SnackBarAction(
            label: 'OK',
            onPressed: () {
              ScaffoldMessenger.of(context).hideCurrentSnackBar();
              context.read<AppCubit>().onFinishTutorial(tutorialId);
              // context.read<AppCubit>().warningViewed();
            },
          ),
        ),
      );
      final SlidableController? slidable = Slidable.of(context);

      slidable?.openEndActionPane(
        duration: const Duration(milliseconds: 300),
        curve: Curves.decelerate,
      );

      Future<void>.delayed(const Duration(seconds: 1), () {
        slidable?.close(
          duration: const Duration(milliseconds: 300),
          curve: Curves.bounceInOut,
        );
      });
    }
  }

  static String tutorialId = 'slidable_tutorial';

  @override
  Widget build(_) =>
      contactToListItem(widget.contact, widget.index, widget.context,
          onTap: widget.onTap,
          onLongPress: widget.onLongPress,
          trailing: widget.trailing);
}

Widget contactToListItem(Contact contact, int index, BuildContext context,
vjrj's avatar
vjrj committed
    {VoidCallback? onTap, VoidCallback? onLongPress, Widget? trailing}) {
vjrj's avatar
vjrj committed
  final String title = contact.title;
  final Widget? subtitle =
      contact.subtitle != null ? Text(contact.subtitle!) : null;
  return ListTile(
      title: Text(title),
      subtitle: subtitle ?? Container(),
vjrj's avatar
vjrj committed
      tileColor: tileColor(index, context),
      onTap: onTap,
vjrj's avatar
vjrj committed
      onLongPress: onLongPress,
vjrj's avatar
vjrj committed
      leading: avatar(
        contact.avatar,
        bgColor: tileColor(index, context),
        color: tileColor(index, context, true),
      ),
      trailing: trailing);
}
vjrj's avatar
vjrj committed

bool showShare() => onlyInDevelopment || !kIsWeb;
vjrj's avatar
vjrj committed

vjrj's avatar
vjrj committed
bool get onlyInDevelopment => !inProduction;
vjrj's avatar
vjrj committed
bool get inDevelopment => !inProduction;
vjrj's avatar
vjrj committed

vjrj's avatar
vjrj committed
bool get onlyInProduction => kReleaseMode;
vjrj's avatar
vjrj committed
bool get inProduction => onlyInProduction;
String assets(String str) =>
    (kIsWeb && kReleaseMode) || (!kIsWeb && Platform.isAndroid)
        ? 'assets/$str'
        : str;
vjrj's avatar
vjrj committed

Future<Directory?> getAppSpecificExternalFilesDirectory(
    [bool ext = false]) async {
  if (ext) {
    final Directory? appSpecificExternalFilesDir =
        await getExternalStorageDirectory();
    return appSpecificExternalFilesDir;
  }
  return getExternalStorageDirectory();
}
vjrj's avatar
vjrj committed

ImageIcon get g1nkgoIcon => ImageIcon(
      AssetImage(ginkgoIconLocation),
      size: 24,
    );

String get ginkgoIconLocation => assets('img/favicon.png');

String capitalize(String s) => s[0].toUpperCase() + s.substring(1);
vjrj's avatar
vjrj committed

double calculate({required String textInTerminal, required String decimalSep}) {
  String operation = textInTerminal;
  double sum = 0.0;
  operation = operation.replaceAll(
      decimalSep, '.'); // change decimal separator to a dot
  final RegExp regex = RegExp(r'[\d.]+'); // regular expression to find numbers
  final Iterable<Match> matches =
      regex.allMatches(operation); // find all numbers in the input
  for (final Match? match in matches) {
    try {
      if (match != null) {
        final String? g1 = match.group(0);
        if (g1 != null) {
          sum += double.parse(g1); // add the number to the sum
        }
      }
    } catch (e) {
      // could not convert the number to a double value, ignore it
    }
  }
  // logger(numberFormat.format(sum)); // print the formatted sum
  return sum;
}

String decimalSep(BuildContext context) {
  return NumberFormat.decimalPattern(eo(currentLocale(context)))
vjrj's avatar
vjrj committed
      .symbols
      .DECIMAL_SEP;
}

Color selectedPatternLock() => Colors.red;
Color notSelectedPatternLock() => Colors.amber;
vjrj's avatar
vjrj committed

String ginkgoNetIcon =
    'https://git.duniter.org/vjrj/ginkgo/-/raw/master/web/icons/favicon-32x32.png';
vjrj's avatar
vjrj committed

final GlobalKey<ScaffoldMessengerState> globalMessengerKey =
    GlobalKey<ScaffoldMessengerState>();
vjrj's avatar
vjrj committed

const Color deleteColor = Color(0xFFFE4A49);
vjrj's avatar
vjrj committed
const Color positiveAmountColor = Colors.blue;
const Color negativeAmountColor = Colors.red;

bool isSymbolPlacementBefore(String pattern) {
  final int symbolIndex = pattern.indexOf('\u00A4');
  final int numberIndex = pattern.indexOf('#');

  if (symbolIndex < numberIndex) {
    return true;
  } else {
    return false;
  }
}

String currentLocale(BuildContext context) => context.locale.languageCode;

String? validateDecimal(
    {required String sep,
    required String locale,
    required String? amount,
    required String Function(String s) tr}) {
  final NumberFormat format = NumberFormat.decimalPattern(eo(locale));
  if (amount == null || amount.isEmpty || amount.startsWith(sep)) {
    return null;
  }
  try {
    final num n = format.parse(amount);
    if (n < 0) {
      return tr('enter_a_positive_number');
    }
    final String formattedAmount = format.format(n);
    if (amount.contains(sep) && amount.endsWith('0')) {
      // remove trailing zeros in 0.10 == 0.1
      amount = amount.replaceAll(RegExp(r'0*$'), '');
    }
    if (formattedAmount != amount) {
      return tr('enter_a_valid_number');
    }
  } catch (e) {
    return tr('enter_a_valid_number');
  }
  return null;
}

Future<bool> openUrl(String url) async {
  final Uri uri = Uri.parse(url);
  return await canLaunchUrl(uri)
      ? await launchUrl(uri, mode: LaunchMode.externalNonBrowserApplication)
      : throw Exception('Could not launch $url');
}
vjrj's avatar
vjrj committed

void showQrDialog({
  required BuildContext context,
  required String publicKey,
  bool noTitle = false,
  String? feedbackText,
}) {
  showDialog(
    context: context,
    builder: (BuildContext context) {
      return Dialog(
        child: SingleChildScrollView(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              SizedBox(
                height: MediaQuery.of(context).size.width * 0.8,
                child: Padding(
                  padding: const EdgeInsets.all(16.0),
                  child: GestureDetector(
                    onTap: () => copyPublicKeyToClipboard(
                        context, publicKey, feedbackText),
                    child: Container(
vjrj's avatar
vjrj committed
                      color:
                          isDark(context) ? Colors.grey[900] : Colors.grey[100],
vjrj's avatar
vjrj committed
                      padding: const EdgeInsets.all(16.0),
                      child: Column(
                        children: <Widget>[
                          if (!noTitle) Text(tr('show_qr_to_client')),
                          if (!noTitle) const SizedBox(height: 10),
                          Expanded(
vjrj's avatar
vjrj committed
                              child: Column(
                                  mainAxisAlignment: MainAxisAlignment.center,
                                  children: <Widget>[
vjrj's avatar
vjrj committed
                                QrImageView(
                                  // version: QrVersions.auto,
vjrj's avatar
vjrj committed
                                  data: publicKey,
                                  size: MediaQuery.of(context).size.width * 0.5,
vjrj's avatar
vjrj committed
                                  // gapless: false,
                                  /* embeddedImage: const AssetImage(
                                    'assets/img/gbrevedot_color.png',
                                  ),
                                  embeddedImageStyle:
                                      const QrEmbeddedImageStyle(
                                    size: Size(80, 80),
                                  ), */
                                  eyeStyle: QrEyeStyle(
                                      eyeShape: QrEyeShape.square,
vjrj's avatar
vjrj committed
                                      color: isDark(context)
                                          ? Theme.of(context).hintColor
                                          : Theme.of(context).primaryColor),
vjrj's avatar
vjrj committed
                                  dataModuleStyle: QrDataModuleStyle(
                                      dataModuleShape: QrDataModuleShape.square,
vjrj's avatar
vjrj committed
                                      color: isDark(context)
vjrj's avatar
vjrj committed
                                          ? Colors.white
vjrj's avatar
vjrj committed
                                          : Colors.black),
vjrj's avatar
vjrj committed
                                )
                              ])),
vjrj's avatar
vjrj committed
                        ],
                      ),
                    ),
                  ),
                ),
              ),
              Padding(
                padding: const EdgeInsets.all(12.0),
                child: TextFormField(
                  maxLines: 2,
                  initialValue: publicKey,
                  readOnly: true,
                  decoration: InputDecoration(
                    suffixIcon: IconButton(
                      icon: const Icon(Icons.content_copy),
                      onPressed: () {
                        copyPublicKeyToClipboard(
                            context, publicKey, feedbackText);
                      },
                    ),
                  ),
                ),
              ),
              const SizedBox(height: 20),
            ],
          ),
        ),
      );
    },
  );
}
vjrj's avatar
vjrj committed

vjrj's avatar
vjrj committed
bool isDark(BuildContext context) =>
    Theme.of(context).brightness == Brightness.dark;

vjrj's avatar
vjrj committed
bool get isIOS => !kIsWeb && Platform.isIOS;
const String g1nkgoUserNameSuffix = ' ❥';
const String protectedUserNameSuffix = ' 🔒';
vjrj's avatar
vjrj committed
const double cardAspectRatio = 1.58;

Future<void> hydratedInit() async {
  await Hive.initFlutter();

  // Reset hive old keys
  if (kIsWeb) {
    final Box<dynamic> box = await Hive.openBox('hydrated_box',
        path: HydratedStorage.webStorageDirectory.path);
    final List<dynamic> keysToDelete =
        box.keys.where((dynamic key) => '$key'.startsWith('minified')).toList();
    box.deleteAll(keysToDelete);
    // This should we done after init
    // await HydratedBloc.storage.clear();
    box.close();
  }

  if (kIsWeb) {
    HydratedBloc.storage = await HydratedStorage.build(
        storageDirectory: HydratedStorage.webStorageDirectory);
  } else {
    final Directory tmpDir = await getTemporaryDirectory();
    Hive.init(tmpDir.toString());
    HydratedBloc.storage =
        await HydratedStorage.build(storageDirectory: tmpDir);
  }
}
vjrj's avatar
vjrj committed

String buildTxNotifTitle(String? from) {
  final String title = from != null
      ? tr('notification_new_payment_title')
      : tr('notification_new_sent_title');
  return title;
}

String buildTxNotifDescription({
  required String? from,
  required String? to,
  required String? comment,
  required String localeLanguageCode,
  required double amount,
  required bool isG1,
  required double currentUd,
}) {
  final String formattedAmount = formatKAmountInViewWithLocale(
    locale: localeLanguageCode,
    amount: amount,
    isG1: isG1,
    currentUd: currentUd,
    useSymbol: true,
  );
  final String desc = from != null
      ? tr('notification_new_payment_desc', namedArgs: <String, String>{
          'amount': formattedAmount,
          'from': from,
        })
      : tr('notification_new_sent_desc',
          namedArgs: <String, String>{'amount': formattedAmount, 'to': to!});

  return comment != null && comment.isNotEmpty ? '$desc ($comment)' : desc;
}