zwallet/lib/account.dart

973 lines
32 KiB
Dart
Raw Normal View History

2021-06-26 07:30:12 -07:00
import 'dart:async';
2021-09-11 02:35:48 -07:00
import 'dart:ui';
2021-06-26 07:30:12 -07:00
2021-08-05 06:43:05 -07:00
import 'package:file_picker/file_picker.dart';
2021-06-26 07:30:12 -07:00
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
2021-07-10 22:20:53 -07:00
import 'package:flutter/services.dart';
2021-08-09 07:13:42 -07:00
import 'package:flutter_form_builder/flutter_form_builder.dart';
2021-06-26 07:30:12 -07:00
import 'package:flutter_mobx/flutter_mobx.dart';
2021-08-09 07:13:42 -07:00
import 'package:intl/intl.dart';
2021-06-26 07:30:12 -07:00
import 'package:local_auth/local_auth.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
2021-06-26 07:30:12 -07:00
import 'package:qr_flutter/qr_flutter.dart';
2021-07-07 08:40:05 -07:00
import 'package:warp/store.dart';
2021-06-26 07:30:12 -07:00
import 'package:warp_api/warp_api.dart';
import 'about.dart';
2021-08-16 06:07:16 -07:00
import 'chart.dart';
2021-09-08 07:14:19 -07:00
import 'contact.dart';
2021-06-26 07:30:12 -07:00
import 'main.dart';
2021-08-15 09:18:09 -07:00
import 'generated/l10n.dart';
2021-06-26 07:30:12 -07:00
class AccountPage extends StatefulWidget {
@override
State<StatefulWidget> createState() => _AccountPageState();
}
class _AccountPageState extends State<AccountPage>
2021-07-10 22:20:53 -07:00
with
WidgetsBindingObserver,
AutomaticKeepAliveClientMixin,
SingleTickerProviderStateMixin {
2021-09-10 02:56:15 -07:00
Timer? _timerSync;
2021-07-07 08:40:05 -07:00
int _progress = 0;
2021-07-07 21:22:54 -07:00
bool _useSnapAddress = false;
String _snapAddress = "";
2021-09-10 02:56:15 -07:00
late TabController _tabController;
2021-07-10 22:20:53 -07:00
bool _accountTab = true;
2021-09-08 07:14:19 -07:00
bool _contactsTab = false;
2021-09-10 02:56:15 -07:00
StreamSubscription? _progressDispose;
StreamSubscription? _syncDispose;
2021-09-08 07:14:19 -07:00
final contactKey = GlobalKey<ContactsState>();
2021-06-26 07:30:12 -07:00
@override
bool get wantKeepAlive => true;
@override
initState() {
2021-07-07 08:40:05 -07:00
super.initState();
2021-08-09 07:13:42 -07:00
_tabController = TabController(length: 6, vsync: this);
2021-07-10 22:20:53 -07:00
_tabController.addListener(() {
setState(() {
_accountTab = _tabController.index == 0;
2021-09-08 07:14:19 -07:00
_contactsTab = _tabController.index == 5;
2021-07-10 22:20:53 -07:00
});
});
2021-06-26 07:30:12 -07:00
Future.microtask(() async {
2021-07-07 08:40:05 -07:00
await accountManager.updateUnconfirmedBalance();
2021-09-05 06:06:54 -07:00
await accountManager.fetchAccountData(false);
2021-09-08 07:14:19 -07:00
await contacts.fetchContacts();
2021-07-19 01:17:23 -07:00
await _setupTimer();
2021-06-26 07:30:12 -07:00
});
2021-09-10 02:56:15 -07:00
WidgetsBinding.instance?.addObserver(this);
_progressDispose = progressStream.listen((percent) {
2021-07-07 08:40:05 -07:00
setState(() {
_progress = percent;
});
});
_syncDispose = syncStream.listen((height) {
setState(() {
2021-09-05 06:06:54 -07:00
if (height >= 0) {
syncStatus.setSyncHeight(height);
2021-09-05 06:06:54 -07:00
eta.checkpoint(height, DateTime.now());
} else {
WarpApi.mempoolReset(syncStatus.latestHeight);
_trySync();
}
});
});
2021-06-26 07:30:12 -07:00
}
@override
void dispose() {
_timerSync?.cancel();
2021-09-10 02:56:15 -07:00
WidgetsBinding.instance?.removeObserver(this);
_progressDispose?.cancel();
_syncDispose?.cancel();
2021-06-26 07:30:12 -07:00
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.detached:
case AppLifecycleState.inactive:
case AppLifecycleState.paused:
_timerSync?.cancel();
break;
case AppLifecycleState.resumed:
_setupTimer();
break;
}
}
@override
Widget build(BuildContext context) {
super.build(context);
if (!syncStatus.isSynced() && !syncStatus.syncing) _trySync();
2021-06-26 07:30:12 -07:00
if (accountManager.active == null) return CircularProgressIndicator();
2021-07-12 04:32:49 -07:00
final theme = Theme.of(this.context);
2021-08-07 00:32:10 -07:00
final hasTAddr = accountManager.taddress.isNotEmpty;
2021-09-09 03:09:14 -07:00
final qrSize = getScreenSize(context) / 2.5;
2021-07-10 22:20:53 -07:00
return Scaffold(
2021-08-09 07:13:42 -07:00
appBar: AppBar(
2021-09-08 19:56:24 -07:00
centerTitle: true,
2021-08-09 07:13:42 -07:00
title: Observer(
2021-09-08 19:56:24 -07:00
builder: (context) => Text("${accountManager.active.name}")),
2021-08-09 07:13:42 -07:00
bottom: TabBar(controller: _tabController, isScrollable: true, tabs: [
2021-08-15 09:18:09 -07:00
Tab(text: S.of(context).account),
Tab(text: S.of(context).notes),
Tab(text: S.of(context).history),
Tab(text: S.of(context).budget),
Tab(text: S.of(context).tradingPl),
Tab(text: S.of(context).contacts),
2021-07-10 22:20:53 -07:00
]),
2021-08-09 07:13:42 -07:00
actions: [
Observer(builder: (context) {
accountManager.canPay;
return PopupMenuButton<String>(
itemBuilder: (context) => [
2021-08-23 19:04:45 -07:00
PopupMenuItem(
child: Text(S.of(context).accounts), value: "Accounts"),
PopupMenuItem(
child: Text(S.of(context).backup), value: "Backup"),
PopupMenuItem(
child: Text(S.of(context).rescan), value: "Rescan"),
2021-08-09 07:13:42 -07:00
if (accountManager.canPay)
2021-08-23 19:04:45 -07:00
PopupMenuItem(
child: Text(S.of(context).coldStorage), value: "Cold"),
2021-08-09 07:13:42 -07:00
if (accountManager.canPay)
2021-08-23 19:04:45 -07:00
PopupMenuItem(
child: Text(S.of(context).multipay), value: "MultiPay"),
PopupMenuItem(
child: Text(S.of(context).broadcast), value: "Broadcast"),
PopupMenuItem(
child: Text(S.of(context).settings), value: "Settings"),
2021-08-15 09:18:09 -07:00
PopupMenuItem(child: Text(S.of(context).about), value: "About"),
2021-08-09 07:13:42 -07:00
],
onSelected: _onMenu,
);
})
],
),
body: TabBarView(controller: _tabController, children: [
SingleChildScrollView(
padding: EdgeInsets.all(20),
child: Center(
child: Column(children: [
2021-09-05 06:06:54 -07:00
Observer(builder: (context) {
final _1 = eta.eta;
final _2 = syncStatus.syncedHeight;
final _3 = syncStatus.latestHeight;
return syncStatus.syncedHeight < 0
? Text("")
2021-09-05 06:06:54 -07:00
: syncStatus.isSynced()
? Text('${syncStatus.syncedHeight}',
style: theme.textTheme.caption)
: Text(
'${syncStatus.syncedHeight} / ${syncStatus.latestHeight} ${eta.eta}',
2021-09-10 02:56:15 -07:00
style: theme.textTheme.caption!
2021-09-05 06:06:54 -07:00
.apply(color: theme.primaryColor));
}),
2021-08-09 07:13:42 -07:00
Padding(padding: EdgeInsets.symmetric(vertical: 8)),
Observer(builder: (context) {
final _ = accountManager.active.address;
final address = _address();
2021-09-10 02:56:15 -07:00
final shortAddress = address != "" ? address.substring(0, 16) + "..." + address.substring(address.length - 16) : "";
2021-08-13 20:44:53 -07:00
final showTAddr = accountManager.showTAddr;
2021-08-09 07:13:42 -07:00
return Column(children: [
if (hasTAddr)
Text(showTAddr
2021-08-15 09:18:09 -07:00
? S.of(context).tapQrCodeForShieldedAddress
: S.of(context).tapQrCodeForTransparentAddress),
2021-08-09 07:13:42 -07:00
Padding(padding: EdgeInsets.symmetric(vertical: 4)),
GestureDetector(
onTap: hasTAddr ? _onQRTap : null,
child: QrImage(
data: address,
2021-09-09 03:09:14 -07:00
size: qrSize,
embeddedImage: AssetImage('assets/icon.png'),
2021-08-09 07:13:42 -07:00
backgroundColor: Colors.white)),
Padding(padding: EdgeInsets.symmetric(vertical: 8)),
RichText(
text: TextSpan(children: [
TextSpan(
2021-09-09 08:23:10 -07:00
text: '$shortAddress ', style: theme.textTheme.bodyText2),
2021-08-09 07:13:42 -07:00
WidgetSpan(
child: GestureDetector(
child: Icon(Icons.content_copy),
onTap: _onAddressCopy)),
WidgetSpan(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 4))),
WidgetSpan(
child: GestureDetector(
child: Icon(MdiIcons.qrcodeScan),
onTap: _onReceive)),
2021-08-09 07:13:42 -07:00
])),
2021-08-13 20:44:53 -07:00
Padding(padding: EdgeInsets.symmetric(vertical: 4)),
2021-08-09 07:13:42 -07:00
if (!showTAddr)
2021-08-13 20:44:53 -07:00
OutlinedButton(
2021-08-15 09:18:09 -07:00
child: Text(S.of(context).newSnapAddress),
2021-08-13 20:44:53 -07:00
style: OutlinedButton.styleFrom(
side: BorderSide(
width: 1, color: theme.primaryColor)),
2021-08-09 07:13:42 -07:00
onPressed: _onSnapAddress),
if (showTAddr)
2021-08-13 20:44:53 -07:00
OutlinedButton(
2021-08-15 09:18:09 -07:00
child: Text(S.of(context).shieldTranspBalance),
2021-08-13 20:44:53 -07:00
style: OutlinedButton.styleFrom(
side:
BorderSide(width: 1, color: theme.primaryColor)),
2021-08-09 07:13:42 -07:00
onPressed: _onShieldTAddr,
)
]);
}),
Observer(builder: (context) {
2021-08-13 20:44:53 -07:00
final balance = accountManager.showTAddr
2021-08-09 07:13:42 -07:00
? accountManager.tbalance
: accountManager.balance;
2021-09-09 08:23:10 -07:00
final balanceHi = _getBalance_hi(balance);
2021-09-10 02:56:15 -07:00
final balanceStyle = (balanceHi.length > 9 ? theme.textTheme.headline3 : theme.textTheme.headline2)!.copyWith(color: theme.colorScheme.primaryVariant);
2021-08-09 07:13:42 -07:00
return Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.ideographic,
children: <Widget>[
2021-09-09 08:23:10 -07:00
Text('${coin.symbol}', style: theme.textTheme.headline5),
Text(' $balanceHi',
style: balanceStyle),
2021-08-09 07:13:42 -07:00
Text('${_getBalance_lo(balance)}'),
]);
}),
Observer(builder: (context) {
2021-08-13 20:44:53 -07:00
final balance = accountManager.showTAddr
2021-08-09 07:13:42 -07:00
? accountManager.tbalance
: accountManager.balance;
final fx = _fx();
final balanceFX = balance * fx / ZECUNIT;
return Column(children: [
if (fx != 0.0)
Text("${balanceFX.toStringAsFixed(2)} ${settings.currency}",
style: theme.textTheme.headline6),
if (fx != 0.0)
Text(
2021-08-13 20:44:53 -07:00
"1 ${coin.ticker} = ${fx.toStringAsFixed(2)} ${settings.currency}"),
2021-08-09 07:13:42 -07:00
]);
}),
Padding(padding: EdgeInsets.symmetric(vertical: 8)),
Observer(
builder: (context) => (accountManager.unconfirmedBalance != 0)
? Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.ideographic,
children: <Widget>[
Text(
'${_sign(accountManager.unconfirmedBalance)} ${_getBalance_hi(accountManager.unconfirmedBalance)}',
style: theme.textTheme.headline4
?.merge(_unconfirmedStyle())),
Text(
'${_getBalance_lo(accountManager.unconfirmedBalance)}',
style: _unconfirmedStyle()),
])
: Container()),
if (_progress > 0)
LinearProgressIndicator(value: _progress / 100.0),
]))),
NoteWidget(tabTo),
HistoryWidget(tabTo),
BudgetWidget(),
PnLWidget(),
2021-09-08 07:14:19 -07:00
ContactsTab(key: contactKey),
2021-08-09 07:13:42 -07:00
]),
floatingActionButton: _accountTab
? FloatingActionButton(
onPressed: _onSend,
backgroundColor: Theme.of(context)
.accentColor
.withOpacity(accountManager.canPay ? 1.0 : 0.3),
child: Icon(Icons.send),
2021-09-08 08:06:35 -07:00
)
: _contactsTab
? FloatingActionButton(
onPressed: _onAddContact,
backgroundColor: Theme.of(context).accentColor,
child: Icon(Icons.add),
)
: Container(), // This trailing comma makes auto-formatting nicer for build methods.
2021-08-09 07:13:42 -07:00
);
2021-06-26 07:30:12 -07:00
}
2021-07-12 04:32:49 -07:00
void tabTo(int index) {
if (index != _tabController.index) _tabController.animateTo(index);
}
2021-09-09 03:09:14 -07:00
String _address() {
final address = accountManager.showTAddr
? accountManager.taddress
: (_useSnapAddress
? _uaAddress(_snapAddress, accountManager.taddress, settings.useUA)
: _uaAddress(accountManager.active.address, accountManager.taddress,
settings.useUA));
2021-09-09 08:23:10 -07:00
return address;
2021-09-09 03:09:14 -07:00
}
2021-08-27 02:51:34 -07:00
String _uaAddress(String zaddress, String taddress, bool useUA) =>
useUA ? WarpApi.getUA(zaddress, taddress) : zaddress;
2021-07-10 22:20:53 -07:00
2021-06-26 07:30:12 -07:00
_sign(int b) {
return b < 0 ? '-' : '+';
}
2021-07-09 06:33:39 -07:00
_onQRTap() {
2021-08-07 00:32:10 -07:00
accountManager.toggleShowTAddr();
2021-07-09 06:33:39 -07:00
}
2021-07-10 22:20:53 -07:00
_onAddressCopy() {
Clipboard.setData(ClipboardData(text: _address()));
2021-08-23 19:04:45 -07:00
final snackBar =
SnackBar(content: Text(S.of(context).addressCopiedToClipboard));
2021-09-10 02:56:15 -07:00
rootScaffoldMessengerKey.currentState?.showSnackBar(snackBar);
2021-07-10 22:20:53 -07:00
}
2021-07-09 06:33:39 -07:00
_onShieldTAddr() {
showDialog(
context: this.context,
barrierDismissible: false,
builder: (context) => AlertDialog(
2021-08-15 09:18:09 -07:00
title: Text(S.of(context).shieldTransparentBalance),
content: Text(S
.of(context)
.doYouWantToTransferYourEntireTransparentBalanceTo(coin.ticker)),
2021-08-13 20:44:53 -07:00
actions: confirmButtons(context, () async {
2021-08-23 01:25:37 -07:00
final s = S.of(context);
2021-08-13 20:44:53 -07:00
Navigator.of(this.context).pop();
2021-08-23 19:04:45 -07:00
final snackBar1 = SnackBar(content: Text(s.shieldingInProgress));
2021-09-10 02:56:15 -07:00
rootScaffoldMessengerKey.currentState?.showSnackBar(snackBar1);
2021-08-13 20:44:53 -07:00
final txid = await WarpApi.shieldTAddr(accountManager.active.id);
2021-08-23 01:25:37 -07:00
final snackBar2 = SnackBar(content: Text("${s.txId}: $txid"));
2021-09-10 02:56:15 -07:00
rootScaffoldMessengerKey.currentState?.showSnackBar(snackBar2);
2021-08-13 20:44:53 -07:00
})),
2021-07-09 06:33:39 -07:00
);
}
_onReceive() {
showQR(context, _address());
}
2021-06-26 07:30:12 -07:00
_unconfirmedStyle() {
2021-09-09 03:09:14 -07:00
return TextStyle(color: amountColor(context, accountManager.unconfirmedBalance));
2021-06-26 07:30:12 -07:00
}
_getBalance_hi(int b) {
return ((b.abs() ~/ 100000) / 1000.0).toStringAsFixed(3);
}
_getBalance_lo(b) {
return (b.abs() % 100000).toString().padLeft(5, '0');
}
2021-07-19 01:17:23 -07:00
_setupTimer() async {
await _sync();
2021-06-26 07:30:12 -07:00
_timerSync = Timer.periodic(Duration(seconds: 15), (Timer t) {
_trySync();
});
}
2021-08-07 00:32:10 -07:00
double _fx() {
return priceStore.zecPrice;
}
2021-09-08 08:06:35 -07:00
_sync() async {}
2021-06-26 07:30:12 -07:00
_trySync() async {
priceStore.fetchZecPrice();
if (syncStatus.syncedHeight < 0) return;
await syncStatus.update();
await accountManager.updateUnconfirmedBalance();
if (!syncStatus.isSynced()) {
2021-07-09 22:44:34 -07:00
final res =
await WarpApi.tryWarpSync(settings.getTx, settings.anchorOffset);
2021-06-26 07:30:12 -07:00
if (res == 1) {
// Reorg
final targetHeight = syncStatus.syncedHeight - 10;
WarpApi.rewindToHeight(targetHeight);
syncStatus.setSyncHeight(targetHeight);
} else if (res == 0) {
2021-07-19 01:17:23 -07:00
syncStatus.update();
2021-06-26 07:30:12 -07:00
}
}
2021-09-05 06:06:54 -07:00
await accountManager.fetchAccountData(false);
2021-07-07 08:40:05 -07:00
await accountManager.updateBalance();
2021-06-26 07:30:12 -07:00
await accountManager.updateUnconfirmedBalance();
2021-09-08 07:14:19 -07:00
await contacts.fetchContacts();
2021-07-09 06:33:39 -07:00
accountManager.updateTBalance();
2021-06-26 07:30:12 -07:00
}
2021-07-07 21:22:54 -07:00
_onSnapAddress() {
final address = accountManager.newAddress();
setState(() {
_useSnapAddress = true;
_snapAddress = address;
});
Timer(Duration(seconds: 15), () {
setState(() {
_useSnapAddress = false;
});
});
}
2021-07-09 06:33:39 -07:00
2021-06-26 07:30:12 -07:00
_onSend() {
Navigator.of(this.context).pushNamed('/send');
}
_onMenu(String choice) {
switch (choice) {
case "Accounts":
Navigator.of(this.context).pushNamed('/accounts');
break;
case "Backup":
_backup();
break;
case "Rescan":
_rescan();
break;
2021-07-07 08:40:05 -07:00
case "Cold":
_cold();
break;
2021-08-02 22:58:02 -07:00
case "MultiPay":
_multiPay();
break;
2021-08-05 06:43:05 -07:00
case "Broadcast":
_broadcast();
break;
2021-07-09 06:33:39 -07:00
case "Settings":
_settings();
break;
2021-06-26 07:30:12 -07:00
case "About":
showAbout(this.context);
break;
}
}
_backup() async {
final localAuth = LocalAuthentication();
2021-07-30 05:36:46 -07:00
try {
final didAuthenticate = await localAuth.authenticate(
2021-08-15 09:18:09 -07:00
localizedReason: S.of(context).pleaseAuthenticateToShowAccountSeed);
2021-07-30 05:36:46 -07:00
if (didAuthenticate) {
Navigator.of(context).pushNamed('/backup', arguments: true);
}
2021-08-09 07:13:42 -07:00
} on PlatformException catch (e) {
2021-07-30 05:36:46 -07:00
await showDialog(
2021-08-09 07:13:42 -07:00
context: context,
2021-07-30 05:36:46 -07:00
barrierDismissible: true,
builder: (context) => AlertDialog(
2021-08-15 09:18:09 -07:00
title: Text(S.of(context).noAuthenticationMethod),
2021-09-10 02:56:15 -07:00
content: Text(e.message ?? "")));
2021-06-26 07:30:12 -07:00
}
}
_rescan() {
rescanDialog(context, () {
2021-09-08 08:06:35 -07:00
Navigator.of(context).pop();
syncStatus.sync(context);
});
2021-06-26 07:30:12 -07:00
}
2021-07-07 08:40:05 -07:00
_cold() {
showDialog(
2021-07-30 05:36:46 -07:00
context: context,
2021-07-07 08:40:05 -07:00
barrierDismissible: false,
builder: (context) => AlertDialog(
2021-08-23 19:04:45 -07:00
title: Text(S.of(context).coldStorage),
content:
Text(S.of(context).doYouWantToDeleteTheSecretKeyAndConvert),
actions: confirmButtons(context, _convertToWatchOnly,
okLabel: S.of(context).delete)));
2021-07-07 08:40:05 -07:00
}
2021-08-02 22:58:02 -07:00
_multiPay() {
Navigator.of(context).pushNamed('/multipay');
}
2021-08-05 06:43:05 -07:00
_broadcast() async {
final result = await FilePicker.platform.pickFiles();
if (result != null) {
final res = WarpApi.broadcast(result.files.single.path);
final snackBar = SnackBar(content: Text(res));
2021-09-10 02:56:15 -07:00
rootScaffoldMessengerKey.currentState?.showSnackBar(snackBar);
2021-08-05 06:43:05 -07:00
}
}
2021-07-07 08:40:05 -07:00
_convertToWatchOnly() {
accountManager.convertToWatchOnly();
Navigator.of(context).pop();
}
2021-07-09 06:33:39 -07:00
_settings() {
2021-09-08 07:14:19 -07:00
Navigator.of(context).pushNamed('/settings');
}
_onAddContact() async {
2021-09-08 08:06:35 -07:00
final contact =
2021-09-10 02:56:15 -07:00
await contactKey.currentState?.showContactForm(context, Contact.empty());
2021-09-08 07:14:19 -07:00
if (contact != null) {
contacts.add(contact);
}
2021-07-09 06:33:39 -07:00
}
2021-06-26 07:30:12 -07:00
}
class NoteWidget extends StatefulWidget {
2021-09-08 19:56:24 -07:00
final void Function(int index) tabTo;
2021-07-12 04:32:49 -07:00
NoteWidget(this.tabTo);
2021-06-26 07:30:12 -07:00
@override
State<StatefulWidget> createState() => _NoteState();
}
class _NoteState extends State<NoteWidget> with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true; //Set to true
@override
Widget build(BuildContext context) {
2021-09-08 19:56:24 -07:00
final headerStyle = Theme.of(context).textTheme.headline4;
2021-06-26 07:30:12 -07:00
return SingleChildScrollView(
2021-09-08 19:56:24 -07:00
padding: EdgeInsets.all(8),
2021-06-26 07:30:12 -07:00
scrollDirection: Axis.vertical,
child: Observer(builder: (context) {
var amountHeader = S.of(context).amount;
switch (accountManager.noteSortOrder) {
case SortOrder.Ascending:
amountHeader += ' \u2191';
break;
case SortOrder.Descending:
amountHeader += ' \u2193';
break;
default:
}
return NotificationListener<OverscrollNotification>(
onNotification: (s) {
final os = s.overscroll;
if (os < 0) {
widget.tabTo(0);
return true;
}
if (os > 0) {
widget.tabTo(2);
return true;
}
return false;
},
child: PaginatedDataTable(
columns: [
DataColumn(
label: settings.showConfirmations
? Text(S.of(context).confs)
: Text(S.of(context).height),
onSort: (_, __) {
setState(() {
settings.toggleShowConfirmations();
});
}),
DataColumn(label: Text(S.of(context).datetime)),
DataColumn(
label: Text(amountHeader),
numeric: true,
onSort: (_, __) {
setState(() {
accountManager.sortNoteAmount();
});
}),
],
header: Text(S.of(context).selectNotesToExcludeFromPayments,
2021-09-08 19:56:24 -07:00
style: Theme.of(context).textTheme.bodyText2),
columnSpacing: 16,
showCheckboxColumn: false,
availableRowsPerPage: [5, 10, 25, 100],
2021-09-10 02:56:15 -07:00
onRowsPerPageChanged: (int? value) {
settings.setRowsPerPage(value ?? 25);
2021-07-12 04:32:49 -07:00
},
showFirstLastButtons: true,
rowsPerPage: settings.rowsPerPage,
source: NotesDataSource(context, _onRowSelected),
));
}));
2021-07-12 04:32:49 -07:00
}
_onRowSelected(Note note) {
accountManager.excludeNote(note);
2021-07-10 22:20:53 -07:00
}
}
class NotesDataSource extends DataTableSource {
2021-07-12 04:32:49 -07:00
final BuildContext context;
final Function(Note) onRowSelected;
2021-07-10 22:20:53 -07:00
2021-07-12 04:32:49 -07:00
NotesDataSource(this.context, this.onRowSelected);
2021-07-10 22:20:53 -07:00
@override
DataRow getRow(int index) {
2021-08-16 06:07:16 -07:00
final note = accountManager.sortedNotes[index];
2021-07-18 08:59:02 -07:00
final theme = Theme.of(context);
2021-08-09 07:13:42 -07:00
final confsOrHeight = settings.showConfirmations
? syncStatus.latestHeight - note.height + 1
: note.height;
2021-08-14 01:02:30 -07:00
2021-09-10 02:56:15 -07:00
var style = theme.textTheme.bodyText2!;
2021-09-09 03:09:14 -07:00
if (!_confirmed(note.height))
2021-09-10 02:56:15 -07:00
style = style.copyWith(color: style.color!.withOpacity(0.5));
2021-09-09 03:09:14 -07:00
2021-08-14 01:02:30 -07:00
if (note.spent)
style = style.merge(TextStyle(decoration: TextDecoration.lineThrough));
2021-09-09 08:23:10 -07:00
final amountStyle = fontWeight(style, note.value);
2021-09-09 03:09:14 -07:00
2021-07-12 04:32:49 -07:00
return DataRow.byIndex(
index: index,
selected: note.excluded,
2021-07-18 08:59:02 -07:00
color: MaterialStateColor.resolveWith((states) =>
states.contains(MaterialState.selected)
? theme.primaryColor.withOpacity(0.5)
: theme.backgroundColor),
2021-07-10 22:20:53 -07:00
cells: [
2021-08-23 19:04:45 -07:00
DataCell(Text("$confsOrHeight", style: style)),
2021-08-14 01:02:30 -07:00
DataCell(Text("${note.timestamp}", style: style)),
2021-09-09 08:23:10 -07:00
DataCell(Text("${note.value.toStringAsFixed(8)}", style: amountStyle)),
2021-07-10 22:20:53 -07:00
],
2021-07-12 04:32:49 -07:00
onSelectChanged: (selected) => _noteSelected(note, selected),
2021-07-10 22:20:53 -07:00
);
}
@override
bool get isRowCountApproximate => false;
@override
int get rowCount => accountManager.notes.length;
@override
int get selectedRowCount => 0;
bool _confirmed(int height) {
return syncStatus.latestHeight - height >= settings.anchorOffset;
2021-06-26 07:30:12 -07:00
}
2021-07-12 04:32:49 -07:00
2021-09-10 02:56:15 -07:00
void _noteSelected(Note note, bool? selected) {
2021-07-12 04:32:49 -07:00
note.excluded = !note.excluded;
notifyListeners();
onRowSelected(note);
}
2021-06-26 07:30:12 -07:00
}
class HistoryWidget extends StatefulWidget {
2021-09-09 03:09:14 -07:00
final void Function(int index) tabTo;
2021-07-12 04:32:49 -07:00
HistoryWidget(this.tabTo);
2021-06-26 07:30:12 -07:00
@override
2021-08-09 07:13:42 -07:00
State<StatefulWidget> createState() => HistoryState();
2021-06-26 07:30:12 -07:00
}
2021-08-09 07:13:42 -07:00
class HistoryState extends State<HistoryWidget>
2021-06-26 07:30:12 -07:00
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true; //Set to true
@override
Widget build(BuildContext context) {
super.build(context);
return SingleChildScrollView(
2021-07-12 04:32:49 -07:00
padding: EdgeInsets.all(4),
2021-06-26 07:30:12 -07:00
scrollDirection: Axis.vertical,
child: Observer(builder: (context) {
var amountHeader = S.of(context).amount;
switch (accountManager.txSortOrder) {
case SortOrder.Ascending:
amountHeader += ' \u2191';
break;
case SortOrder.Descending:
amountHeader += ' \u2193';
break;
default:
}
return NotificationListener<OverscrollNotification>(
onNotification: (s) {
if (s.overscroll < 0) {
widget.tabTo(1);
return true;
}
if (s.overscroll > 0) {
widget.tabTo(3);
return true;
}
return false;
},
child: PaginatedDataTable(
columns: [
DataColumn(
label: settings.showConfirmations
? Text(S.of(context).confs)
: Text(S.of(context).height),
onSort: (_, __) {
setState(() {
settings.toggleShowConfirmations();
});
}),
DataColumn(label: Text(S.of(context).datetime)),
DataColumn(label: Text(S.of(context).txId)),
DataColumn(
label: Text(amountHeader),
numeric: true,
onSort: (_, __) {
setState(() {
accountManager.sortTxAmount();
});
}),
],
columnSpacing: 16,
showCheckboxColumn: false,
availableRowsPerPage: [5, 10, 25, 100],
2021-09-10 02:56:15 -07:00
onRowsPerPageChanged: (int? value) {
settings.setRowsPerPage(value ?? 25);
},
showFirstLastButtons: true,
rowsPerPage: settings.rowsPerPage,
source: HistoryDataSource(context)));
}));
2021-06-26 07:30:12 -07:00
}
}
2021-07-10 22:20:53 -07:00
class HistoryDataSource extends DataTableSource {
BuildContext context;
HistoryDataSource(this.context);
@override
DataRow getRow(int index) {
2021-08-16 06:07:16 -07:00
final tx = accountManager.sortedTxs[index];
2021-08-09 07:13:42 -07:00
final confsOrHeight = settings.showConfirmations
? syncStatus.latestHeight - tx.height + 1
: tx.height;
2021-09-09 03:09:14 -07:00
final color = amountColor(context, tx.value);
2021-09-10 02:56:15 -07:00
var style = Theme.of(context).textTheme.bodyText2!.copyWith(color: color);
2021-09-09 03:09:14 -07:00
style = fontWeight(style, tx.value);
2021-07-10 22:20:53 -07:00
return DataRow(
cells: [
2021-07-30 02:45:43 -07:00
DataCell(Text("$confsOrHeight")),
2021-07-10 22:20:53 -07:00
DataCell(Text("${tx.timestamp}")),
DataCell(Text("${tx.txid}")),
2021-09-09 03:09:14 -07:00
DataCell(Text("${tx.value.toStringAsFixed(8)}", style: style,
2021-07-10 22:20:53 -07:00
textAlign: TextAlign.left)),
],
onSelectChanged: (_) {
Navigator.of(this.context).pushNamed('/tx', arguments: tx);
});
}
@override
bool get isRowCountApproximate => false;
@override
int get rowCount => accountManager.txs.length;
@override
int get selectedRowCount => 0;
}
2021-07-12 04:32:49 -07:00
class BudgetWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() => BudgetState();
}
class BudgetState extends State<BudgetWidget>
with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true; //Set to true
@override
Widget build(BuildContext context) {
super.build(context);
return Padding(
padding: EdgeInsets.all(4),
child: Observer(builder: (context) {
2021-07-18 08:59:02 -07:00
final _ = accountManager.dataEpoch;
2021-07-12 04:32:49 -07:00
return Column(
children: [
2021-09-08 08:06:35 -07:00
Card(
2021-07-12 04:32:49 -07:00
child: Column(children: [
2021-08-15 09:18:09 -07:00
Text(S.of(context).largestSpendingsByAddress,
2021-07-12 04:32:49 -07:00
style: Theme.of(context).textTheme.headline6),
2021-09-08 08:06:35 -07:00
SizedBox(
2021-08-16 06:07:16 -07:00
child: PieChartSpending(accountManager.spendings),
2021-09-09 03:09:14 -07:00
height: getScreenSize(context) / 2,
2021-08-16 06:07:16 -07:00
),
2021-08-15 09:18:09 -07:00
Text(S.of(context).tapChartToToggleBetweenAddressAndAmount,
2021-07-12 04:32:49 -07:00
style: Theme.of(context).textTheme.caption)
2021-09-08 08:06:35 -07:00
])),
2021-07-12 04:32:49 -07:00
Expanded(
child: Card(
child: Column(children: [
2021-08-15 09:18:09 -07:00
Text(S.of(context).accountBalanceHistory,
2021-07-12 04:32:49 -07:00
style: Theme.of(context).textTheme.headline6),
Expanded(
2021-08-16 06:07:16 -07:00
child: LineChartTimeSeries(accountManager.accountBalances))
2021-07-12 04:32:49 -07:00
]))),
],
);
}));
}
}
2021-07-18 08:59:02 -07:00
2021-08-09 07:13:42 -07:00
class PnLWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() => PnLState();
}
class PnLState extends State<PnLWidget> with AutomaticKeepAliveClientMixin {
@override
bool get wantKeepAlive => true; //Set to true
@override
Widget build(BuildContext context) {
return Column(children: [
FormBuilderRadioGroup(
orientation: OptionsOrientation.horizontal,
2021-08-15 09:18:09 -07:00
name: S.of(context).pnl,
2021-08-09 07:13:42 -07:00
initialValue: accountManager.pnlSeriesIndex,
2021-09-10 02:56:15 -07:00
onChanged: (int? v) {
2021-08-09 07:13:42 -07:00
setState(() {
2021-09-10 02:56:15 -07:00
accountManager.setPnlSeriesIndex(v!);
2021-08-09 07:13:42 -07:00
});
},
options: [
2021-08-23 19:04:45 -07:00
FormBuilderFieldOption(
child: Text(S.of(context).realized), value: 0),
2021-08-15 09:18:09 -07:00
FormBuilderFieldOption(child: Text(S.of(context).mm), value: 1),
FormBuilderFieldOption(child: Text(S.of(context).total), value: 2),
FormBuilderFieldOption(child: Text(S.of(context).price), value: 3),
FormBuilderFieldOption(child: Text(S.of(context).qty), value: 4),
FormBuilderFieldOption(child: Text(S.of(context).table), value: 5),
2021-08-09 07:13:42 -07:00
]),
Observer(builder: (context) {
2021-08-23 19:04:45 -07:00
final _ = accountManager.pnlSorted;
2021-08-09 07:13:42 -07:00
return Expanded(
child: accountManager.pnlSeriesIndex != 5
2021-08-16 06:07:16 -07:00
? PnLChart(accountManager.pnls, accountManager.pnlSeriesIndex)
2021-08-09 07:13:42 -07:00
: PnLTable());
})
]);
}
}
class PnLChart extends StatelessWidget {
2021-08-16 06:07:16 -07:00
final List<PnL> pnls;
final int seriesIndex;
2021-08-09 07:13:42 -07:00
2021-08-16 06:07:16 -07:00
PnLChart(this.pnls, this.seriesIndex);
2021-08-09 07:13:42 -07:00
@override
Widget build(BuildContext context) {
2021-08-16 06:07:16 -07:00
final series = _createSeries(pnls, seriesIndex, context);
return LineChartTimeSeries(series);
2021-08-09 07:13:42 -07:00
}
2021-08-16 06:07:16 -07:00
static double _seriesData(PnL pnl, int index) {
2021-08-09 07:13:42 -07:00
switch (index) {
case 0:
return pnl.realized;
case 1:
return pnl.unrealized;
case 2:
return pnl.realized + pnl.unrealized;
case 3:
return pnl.price;
case 4:
return pnl.amount;
}
2021-09-10 02:56:15 -07:00
return 0.0;
2021-08-09 07:13:42 -07:00
}
2021-08-16 06:07:16 -07:00
static List<TimeSeriesPoint<double>> _createSeries(
List<PnL> data, int index, BuildContext context) {
2021-08-23 19:04:45 -07:00
return data
.map((pnl) => TimeSeriesPoint(
pnl.timestamp.millisecondsSinceEpoch ~/ DAY_MS,
_seriesData(pnl, index)))
.toList();
2021-08-09 07:13:42 -07:00
}
}
class PnLTable extends StatelessWidget {
@override
Widget build(BuildContext context) {
2021-08-23 19:04:45 -07:00
final sortSymbol = accountManager.pnlDesc ? ' \u2193' : ' \u2191';
2021-08-09 07:13:42 -07:00
return SingleChildScrollView(
child: Observer(
builder: (context) => PaginatedDataTable(
columns: [
DataColumn(
label: Text(S.of(context).datetime + sortSymbol),
onSort: (_, __) {
accountManager.togglePnlDesc();
}),
DataColumn(label: Text(S.of(context).qty), numeric: true),
DataColumn(label: Text(S.of(context).price), numeric: true),
DataColumn(
label: Text(S.of(context).realized), numeric: true),
DataColumn(label: Text(S.of(context).mm), numeric: true),
DataColumn(label: Text(S.of(context).total), numeric: true),
],
columnSpacing: 16,
showCheckboxColumn: false,
availableRowsPerPage: [5, 10, 25, 100],
2021-09-10 02:56:15 -07:00
onRowsPerPageChanged: (int? value) {
settings.setRowsPerPage(value ?? 25);
},
showFirstLastButtons: true,
rowsPerPage: settings.rowsPerPage,
source: PnLDataSource(context))));
2021-08-09 07:13:42 -07:00
}
}
class PnLDataSource extends DataTableSource {
BuildContext context;
final dateFormat = DateFormat("MM-dd");
PnLDataSource(this.context);
@override
DataRow getRow(int index) {
2021-08-23 19:04:45 -07:00
final pnl = accountManager.pnlSorted[index];
2021-08-09 07:13:42 -07:00
final ts = dateFormat.format(pnl.timestamp);
return DataRow(cells: [
DataCell(Text("$ts")),
DataCell(Text("${pnl.amount.toStringAsFixed(2)}")),
DataCell(Text("${pnl.price.toStringAsFixed(3)}")),
DataCell(Text("${pnl.realized.toStringAsFixed(3)}")),
DataCell(Text("${pnl.unrealized.toStringAsFixed(3)}")),
DataCell(Text("${(pnl.realized + pnl.unrealized).toStringAsFixed(3)}")),
]);
}
@override
bool get isRowCountApproximate => false;
@override
int get rowCount => accountManager.pnls.length;
@override
int get selectedRowCount => 0;
}
2021-09-08 07:14:19 -07:00
// TODO: Refresh contacts after rescan
// Remove Save to BC button after commit
// Truncate tables on rescan