306 lines
9.8 KiB
Dart
306 lines
9.8 KiB
Dart
import 'dart:math';
|
|
import 'dart:ui';
|
|
|
|
import 'package:currency_text_input_formatter/currency_text_input_formatter.dart';
|
|
import 'package:decimal/decimal.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_barcode_scanner/flutter_barcode_scanner.dart';
|
|
import 'package:flutter_mobx/flutter_mobx.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:path/path.dart';
|
|
import 'package:qr_flutter/qr_flutter.dart';
|
|
import 'package:sqflite/sqflite.dart';
|
|
import 'package:warp_api/warp_api.dart';
|
|
import 'package:flutter_localizations/flutter_localizations.dart';
|
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
|
import 'generated/l10n.dart';
|
|
|
|
import 'account.dart';
|
|
import 'account_manager.dart';
|
|
import 'backup.dart';
|
|
import 'coin/coindef.dart';
|
|
import 'multisend.dart';
|
|
import 'settings.dart';
|
|
import 'restore.dart';
|
|
import 'send.dart';
|
|
import 'store.dart';
|
|
import 'transaction.dart';
|
|
|
|
var coin = Coin();
|
|
|
|
const ZECUNIT = 100000000.0;
|
|
var ZECUNIT_DECIMAL = Decimal.parse('100000000');
|
|
const mZECUNIT = 100000;
|
|
const DEFAULT_FEE = 1000;
|
|
|
|
var accountManager = AccountManager();
|
|
var priceStore = PriceStore();
|
|
var syncStatus = SyncStatus();
|
|
var settings = Settings();
|
|
var multipayData = MultiPayStore();
|
|
var eta = ETAStore();
|
|
var contacts = ContactStore();
|
|
|
|
Future<Database> getDatabase() async {
|
|
var databasesPath = await getDatabasesPath();
|
|
final path = join(databasesPath, 'zec.db');
|
|
var db = await openDatabase(path);
|
|
return db;
|
|
}
|
|
|
|
void main() {
|
|
WidgetsFlutterBinding.ensureInitialized();
|
|
final home = ZWalletApp();
|
|
runApp(FutureBuilder(
|
|
future: settings.restore(),
|
|
builder: (context, snapshot) {
|
|
return snapshot.connectionState == ConnectionState.waiting
|
|
? MaterialApp(home: Container()) :
|
|
Observer(builder: (context) {
|
|
final theme = settings.themeData.copyWith(
|
|
dataTableTheme: DataTableThemeData(
|
|
headingRowColor: MaterialStateColor.resolveWith(
|
|
(_) => settings.themeData.highlightColor)));
|
|
return MaterialApp(
|
|
title: coin.app,
|
|
theme: theme,
|
|
home: home,
|
|
scaffoldMessengerKey: rootScaffoldMessengerKey,
|
|
localizationsDelegates: [
|
|
S.delegate,
|
|
GlobalMaterialLocalizations.delegate,
|
|
GlobalWidgetsLocalizations.delegate,
|
|
GlobalCupertinoLocalizations.delegate,
|
|
],
|
|
supportedLocales: S.delegate.supportedLocales,
|
|
onGenerateRoute: (RouteSettings settings) {
|
|
var routes = <String, WidgetBuilder>{
|
|
'/account': (context) => AccountPage(),
|
|
'/restore': (context) => RestorePage(),
|
|
'/send': (context) =>
|
|
SendPage(settings.arguments as Contact?),
|
|
'/accounts': (context) => AccountManagerPage(),
|
|
'/settings': (context) => SettingsPage(),
|
|
'/tx': (context) =>
|
|
TransactionPage(settings.arguments as Tx),
|
|
'/backup': (context) => BackupPage(settings.arguments as int?),
|
|
'/multipay': (context) => MultiPayPage(),
|
|
};
|
|
return MaterialPageRoute(builder: routes[settings.name]!);
|
|
},
|
|
);
|
|
});
|
|
}));
|
|
}
|
|
|
|
class ZWalletApp extends StatefulWidget {
|
|
@override
|
|
State<StatefulWidget> createState() => ZWalletAppState();
|
|
}
|
|
|
|
class ZWalletAppState extends State<ZWalletApp> {
|
|
bool initialized = false;
|
|
|
|
Future<bool> _init() async {
|
|
if (!initialized) {
|
|
initialized = true;
|
|
final dbPath = await getDatabasesPath();
|
|
WarpApi.initWallet(dbPath + "/zec.db", settings.getLWD());
|
|
final db = await getDatabase();
|
|
await accountManager.init(db);
|
|
await contacts.init(db);
|
|
|
|
await syncStatus.init();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return FutureBuilder(
|
|
future: _init(),
|
|
builder: (context, snapshot) {
|
|
if (!snapshot.hasData) return Container();
|
|
return accountManager.accounts.isNotEmpty
|
|
? AccountPage()
|
|
: AccountManagerPage();
|
|
});
|
|
}
|
|
}
|
|
|
|
final GlobalKey<ScaffoldMessengerState> rootScaffoldMessengerKey =
|
|
GlobalKey<ScaffoldMessengerState>();
|
|
|
|
List<ElevatedButton> confirmButtons(
|
|
BuildContext context, VoidCallback? onPressed,
|
|
{String? okLabel, Icon? okIcon, cancelValue}) {
|
|
final navigator = Navigator.of(context);
|
|
return <ElevatedButton>[
|
|
ElevatedButton.icon(
|
|
icon: Icon(Icons.cancel),
|
|
label: Text(S.of(context).cancel),
|
|
onPressed: () {
|
|
cancelValue != null ? navigator.pop(cancelValue) : navigator.pop();
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
primary: Theme.of(context).buttonTheme.colorScheme!.secondary)),
|
|
ElevatedButton.icon(
|
|
icon: okIcon ?? Icon(Icons.done),
|
|
label: Text(okLabel ?? S.of(context).ok),
|
|
onPressed: onPressed,
|
|
)
|
|
];
|
|
}
|
|
|
|
List<TimeSeriesPoint<V>> sampleDaily<T, Y, V>(
|
|
List<T> timeseries,
|
|
int start,
|
|
int end,
|
|
int Function(T) getDay,
|
|
Y Function(T) getY,
|
|
V Function(V, Y) accFn,
|
|
V initial) {
|
|
assert(start % DAY_MS == 0);
|
|
final s = start ~/ DAY_MS;
|
|
final e = end ~/ DAY_MS;
|
|
|
|
var acc = initial;
|
|
var j = 0;
|
|
while (j < timeseries.length && getDay(timeseries[j]) < s) {
|
|
acc = accFn(acc, getY(timeseries[j]));
|
|
j += 1;
|
|
}
|
|
|
|
List<TimeSeriesPoint<V>> ts = [];
|
|
|
|
for (var i = s; i <= e; i++) {
|
|
while (j < timeseries.length && getDay(timeseries[j]) == i) {
|
|
acc = accFn(acc, getY(timeseries[j]));
|
|
j += 1;
|
|
}
|
|
ts.add(TimeSeriesPoint(i, acc));
|
|
}
|
|
return ts;
|
|
}
|
|
|
|
String unwrapUA(String address) {
|
|
final zaddr = WarpApi.getSaplingFromUA(address);
|
|
return zaddr.isNotEmpty ? zaddr : address;
|
|
}
|
|
|
|
void showQR(BuildContext context, String text) {
|
|
showDialog(
|
|
context: context,
|
|
barrierColor: Colors.black,
|
|
builder: (context) => AlertDialog(
|
|
content: Container(
|
|
width: double.maxFinite,
|
|
child: QrImage(data: text, backgroundColor: Colors.white)),
|
|
));
|
|
}
|
|
|
|
Future<void> rescanDialog(
|
|
BuildContext context, VoidCallback continuation) async {
|
|
await showDialog(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (context) => AlertDialog(
|
|
title: Text(S.of(context).rescan),
|
|
content: Text(S.of(context).rescanWalletFromTheFirstBlock),
|
|
actions: confirmButtons(context, () => confirmWifi(context, continuation))));
|
|
}
|
|
|
|
Future<void> confirmWifi(BuildContext context, VoidCallback continuation) async {
|
|
final connectivity = await Connectivity().checkConnectivity();
|
|
if (connectivity == ConnectivityResult.mobile) {
|
|
Navigator.of(context).pop();
|
|
await showDialog(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (context) => AlertDialog(
|
|
title: Text(S.of(context).rescan),
|
|
content: Text('On Mobile Data, scanning may incur additional charges. Do you want to proceed?'),
|
|
actions: confirmButtons(context, continuation)));
|
|
}
|
|
else
|
|
continuation();
|
|
}
|
|
|
|
Future<bool> showMessageBox(
|
|
BuildContext context, String title, String content, String label) async {
|
|
final confirm = await showDialog<bool>(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (context) => AlertDialog(
|
|
title: Text(title),
|
|
content: Text(content),
|
|
actions: confirmButtons(context, () {
|
|
Navigator.of(context).pop(true);
|
|
}, okLabel: label, cancelValue: false)),
|
|
);
|
|
return confirm ?? false;
|
|
}
|
|
|
|
double getScreenSize(BuildContext context) {
|
|
final size = MediaQuery.of(context).size;
|
|
return min(size.height, size.width);
|
|
}
|
|
|
|
Color amountColor(BuildContext context, num a) {
|
|
final theme = Theme.of(context);
|
|
if (a < 0) return Colors.red;
|
|
if (a > 0) return Colors.green;
|
|
return theme.textTheme.bodyText1!.color!;
|
|
}
|
|
|
|
TextStyle fontWeight(TextStyle style, num v) {
|
|
final value = v.abs();
|
|
final style2 = style.copyWith(fontFeatures: [FontFeature.tabularFigures()]);
|
|
if (value >= coin.weights[2])
|
|
return style2.copyWith(fontWeight: FontWeight.w800);
|
|
else if (value >= coin.weights[1])
|
|
return style2.copyWith(fontWeight: FontWeight.w600);
|
|
else if (value >= coin.weights[0]) return style2.copyWith(fontWeight: FontWeight.w400);
|
|
return style2.copyWith(fontWeight: FontWeight.w200);
|
|
}
|
|
|
|
CurrencyTextInputFormatter makeInputFormatter(bool mZEC) =>
|
|
CurrencyTextInputFormatter(symbol: '', decimalDigits: precision(mZEC));
|
|
|
|
double parseNumber(String? s) {
|
|
if (s == null || s.isEmpty) return 0;
|
|
return NumberFormat.currency().parse(s).toDouble();
|
|
}
|
|
|
|
int precision(bool mZEC) => mZEC ? 3 : 8;
|
|
|
|
Future<String?> scanCode(BuildContext context) async {
|
|
final code = await FlutterBarcodeScanner.scanBarcode('#FF0000', S.of(context).cancel, true, ScanMode.QR);
|
|
if (code == "-1") return null;
|
|
return code;
|
|
}
|
|
|
|
String addressLeftTrim(String address) =>
|
|
address != "" ? address.substring(0, 8) + "..." + address.substring(address.length - 16) : "";
|
|
|
|
void showSnackBar(String msg) {
|
|
final snackBar = SnackBar(content: Text(msg));
|
|
rootScaffoldMessengerKey.currentState?.showSnackBar(snackBar);
|
|
}
|
|
|
|
enum DeviceWidth {
|
|
xs, sm, md, lg, xl
|
|
}
|
|
|
|
DeviceWidth getWidth(BuildContext context) {
|
|
final width = MediaQuery.of(context).size.width;
|
|
if (width < 600) return DeviceWidth.xs;
|
|
if (width < 960) return DeviceWidth.sm;
|
|
if (width < 1280) return DeviceWidth.md;
|
|
if (width < 1920) return DeviceWidth.lg;
|
|
return DeviceWidth.xl;
|
|
}
|
|
|
|
String decimalFormat(double x, int decimalDigits, { String symbol = '' }) =>
|
|
NumberFormat.currency(decimalDigits: decimalDigits, symbol: symbol).format(x).trimRight();
|