Skip to content
Snippets Groups Projects
ui_helpers.dart 13 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';
vjrj's avatar
vjrj committed
import 'package:path_provider/path_provider.dart';
vjrj's avatar
vjrj committed
import 'package:timeago/timeago.dart' as timeago;
vjrj's avatar
vjrj committed

import '../data/models/app_cubit.dart';
import '../data/models/contact.dart';
import '../data/models/node_list_cubit.dart';
import '../data/models/transaction_cubit.dart';
import '../g1/api.dart';
vjrj's avatar
vjrj committed
import '../g1/currency.dart';
vjrj's avatar
vjrj committed
import '../shared_prefs.dart';
import 'widgets/first_screen/circular_icon.dart';

vjrj's avatar
vjrj committed
void showTooltip(BuildContext context, String title, String message) {
  showDialog(
    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

void copyPublicKeyToClipboard(BuildContext context) {
vjrj's avatar
vjrj committed
  /* final DataWriterItem item = DataWriterItem();
  item.add(Formats.plainText(SharedPreferencesHelper().getPubKey()));
  ClipboardWriter.instance.write(<DataWriterItem>[item]).then((dynamic value) =>
      ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(tr('key_copied_to_clipboard'))))); */
vjrj's avatar
vjrj committed
  FlutterClipboard.copy(SharedPreferencesHelper().getPubKey()).then(
      (dynamic value) => ScaffoldMessenger.of(context).showSnackBar(
vjrj's avatar
vjrj committed
          SnackBar(content: Text(tr('key_copied_to_clipboard')))));
vjrj's avatar
vjrj committed
}

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

vjrj's avatar
vjrj committed
Widget avatar(Uint8List? rawAvatar,
    {Color color = defAvatarColor,
    Color bgColor = defAvatarBgColor,
    double avatarSize = defAvatarSize}) {
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);
  }
}

String humanizeContact(String publicAddress, Contact contact) {
  final bool hasName = contact.name?.isNotEmpty ?? false;
  final bool hasNick = contact.nick?.isNotEmpty ?? false;

  if (contact.pubKey == publicAddress) {
    return tr('your_wallet');
  } else {
    if (hasName && hasNick)
      return '${contact.name} (${contact.nick})';
    else if (hasNick)
      return contact.nick!;
    else if (hasName)
      return contact.name!;
    else
      return humanizePubKey(contact.pubKey);
  }
}

vjrj's avatar
vjrj committed
String humanizePubKey(String address) => '\u{1F511} ${simplifyPubKey(address)}';

String simplifyPubKey(String address) =>
    address.length <= 8 ? 'WRONG ADDRESS' : address.substring(0, 8);
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

String formatAmount(
    {required BuildContext context,
    required double amount,
    required bool isG1,
    required bool useSymbol}) {
vjrj's avatar
vjrj committed
  return formatAmountWithLocale(
      locale: currentLocale(context),
      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) : '',
vjrj's avatar
vjrj committed
    locale: locale,
vjrj's avatar
vjrj committed
    decimalDigits: isG1 ? 2 : 4,
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 formatKAmount(
        {required BuildContext context,
        required double amount,
        required bool isG1,
        required double currentUd,
        required bool useSymbol}) =>
vjrj's avatar
vjrj committed
    formatAmount(
        context: context,
        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);

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

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

Future<Uint8List?> _getAvatarFromResults(Map<String, dynamic> source) 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 = await resizeAvatar(avatarBase64);
    return avatarBase64resized;
  } else {
    return null;
  }
}

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

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

void fetchTransactions(BuildContext context) {
vjrj's avatar
vjrj committed
  final AppCubit appCubit = context.read<AppCubit>();
  final TransactionCubit transCubit = context.read<TransactionCubit>();
  final NodeListCubit nodeListCubit = context.read<NodeListCubit>();
vjrj's avatar
vjrj committed
  transCubit.fetchTransactions(nodeListCubit, appCubit);
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();
    _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);
}

vjrj's avatar
vjrj committed
ListTile 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(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);

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}) {
  final NumberFormat format = NumberFormat.decimalPattern(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 (formattedAmount != amount) {
      return tr('enter_a_valid_number');
    }
  } catch (e) {
    return tr('enter_a_valid_number');
  }
  return null;
}