zwallet/lib/account.dart

724 lines
24 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';
import 'dart:math';
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';
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';
import 'package:sensors_plus/sensors_plus.dart';
2021-09-25 02:09:41 -07:00
import 'package:warp/payment_uri.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';
import 'budget.dart';
2021-08-16 06:07:16 -07:00
import 'chart.dart';
2021-09-08 07:14:19 -07:00
import 'contact.dart';
2021-09-15 19:40:11 -07:00
import 'history.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-09-17 18:14:08 -07:00
import 'note.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;
StreamSubscription? _accDispose;
2021-09-08 07:14:19 -07:00
final contactKey = GlobalKey<ContactsState>();
bool _flat = false;
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();
}
});
});
_accDispose = accelerometerEvents.listen(_handleAccel);
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();
_accDispose?.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();
_timerSync = null;
_accDispose?.cancel();
_accDispose = null;
2021-06-26 07:30:12 -07:00
break;
case AppLifecycleState.resumed:
2021-09-15 07:42:44 -07:00
if (_timerSync == null) _setupTimer();
if (_accDispose == null)
_accDispose = accelerometerEvents.listen(_handleAccel);
2021-06-26 07:30:12 -07:00
break;
}
}
@override
Widget build(BuildContext context) {
super.build(context);
if (!syncStatus.isSynced() && !syncStatus.syncing) _trySync();
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();
final shortAddress = addressLeftTrim(address);
2021-08-13 20:44:53 -07:00
final showTAddr = accountManager.showTAddr;
final hide = settings.autoHide && _flat;
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,
2021-09-15 07:42:44 -07:00
child: RotatedBox(
quarterTurns: hide ? 2 : 0,
child: QrImage(
data: address,
size: qrSize,
embeddedImage: AssetImage('assets/icon.png'),
backgroundColor: Colors.white))),
2021-08-09 07:13:42 -07:00
Padding(padding: EdgeInsets.symmetric(vertical: 8)),
RichText(
text: TextSpan(children: [
TextSpan(
2021-09-15 07:42:44 -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-09-26 11:44:19 -07:00
onPressed: () { shieldTAddr(context); },
2021-08-09 07:13:42 -07:00
)
]);
}),
Observer(builder: (context) {
final showTAddr = accountManager.showTAddr;
final balance = showTAddr
2021-08-09 07:13:42 -07:00
? accountManager.tbalance
: accountManager.balance;
final hide = settings.autoHide && _flat;
2021-09-23 01:37:43 -07:00
final balanceColor = !showTAddr
? theme.colorScheme.primaryVariant
: theme.colorScheme.secondaryVariant;
final balanceHi = hide ? '-------' : _getBalance_hi(balance);
final deviceWidth = getWidth(context);
final digits = deviceWidth.index < DeviceWidth.sm.index ? 7 : 9;
2021-09-15 07:42:44 -07:00
final balanceStyle = (balanceHi.length > digits
? theme.textTheme.headline4
: theme.textTheme.headline2)!
.copyWith(color: balanceColor);
2021-08-09 07:13:42 -07:00
return Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.ideographic,
children: <Widget>[
2021-09-15 07:42:44 -07:00
if (!hide)
Text('${coin.symbol}',
style: theme.textTheme.headline5),
Text(' $balanceHi', style: balanceStyle),
if (!hide) Text('${_getBalance_lo(balance)}'),
2021-08-09 07:13:42 -07:00
]);
}),
Observer(builder: (context) {
final hide = settings.autoHide && _flat;
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;
2021-09-15 07:42:44 -07:00
return hide
? Text(S.of(context).tiltYourDeviceUpToRevealYourBalance)
: Column(children: [
if (fx != 0.0)
Text(
2021-09-21 05:52:52 -07:00
"${decimalFormat(balanceFX, 2, symbol: settings.currency)}",
2021-09-15 07:42:44 -07:00
style: theme.textTheme.headline6),
if (fx != 0.0)
Text(
2021-09-21 05:52:52 -07:00
"1 ${coin.ticker} = ${decimalFormat(fx, 2, symbol: settings.currency)}"),
2021-09-15 07:42:44 -07:00
]);
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
2021-09-15 07:42:44 -07:00
? _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-09-25 02:09:41 -07:00
_onReceive() async {
2021-09-25 08:03:05 -07:00
await showDialog(context: context,
barrierColor: Colors.black,
builder: (context) =>
Dialog(child: PaymentURIPage(_address())));
}
2021-06-26 07:30:12 -07:00
_unconfirmedStyle() {
2021-09-15 07:42:44 -07:00
return TextStyle(
color: amountColor(context, accountManager.unconfirmedBalance));
2021-06-26 07:30:12 -07:00
}
_getBalance_hi(int b) {
2021-09-21 05:52:52 -07:00
return decimalFormat((b.abs() ~/ 100000) / 1000.0, 3);
2021-06-26 07:30:12 -07:00
}
_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();
await accountManager.updateTBalance();
2021-06-26 07:30:12 -07:00
await accountManager.updateUnconfirmedBalance();
2021-09-08 07:14:19 -07:00
await contacts.fetchContacts();
accountManager.autoshield();
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 {
2021-09-25 18:15:14 -07:00
final didAuthenticate = await authenticate(context, S.of(context).pleaseAuthenticateToShowAccountSeed);
2021-09-25 17:31:39 -07:00
if (didAuthenticate) {
Navigator.of(context).pushNamed('/backup');
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-15 07:42:44 -07:00
final contact = 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-09-15 02:28:07 -07:00
_handleAccel(AccelerometerEvent event) {
2021-09-15 07:42:44 -07:00
final n = sqrt(event.x * event.x + event.y * event.y + event.z * event.z);
final inclination = acos(event.z / n) / pi * 180 * event.y.sign;
2021-09-15 02:28:07 -07:00
final flat = inclination < 20;
if (flat != _flat)
setState(() {
_flat = flat;
});
}
2021-06-26 07:30:12 -07:00
}
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-09-15 07:42:44 -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),
Padding(padding: EdgeInsets.symmetric(vertical: 4)),
BudgetChart(),
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),
Padding(padding: EdgeInsets.symmetric(vertical: 4)),
2021-09-24 00:30:04 -07:00
Expanded(child: Padding(padding: EdgeInsets.only(right: 20),
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-09-14 07:29:40 -07:00
FormBuilderFieldOption(child: Text(S.of(context).price), value: 0),
2021-08-23 19:04:45 -07:00
FormBuilderFieldOption(
2021-09-14 07:29:40 -07:00
child: Text(S.of(context).realized), value: 1),
FormBuilderFieldOption(child: Text(S.of(context).mm), value: 2),
FormBuilderFieldOption(child: Text(S.of(context).total), value: 3),
2021-08-15 09:18:09 -07:00
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(
2021-09-23 01:37:43 -07:00
child: Padding(
padding: EdgeInsets.only(right: 20),
child: accountManager.pnlSeriesIndex != 5
? PnLChart(
accountManager.pnls, accountManager.pnlSeriesIndex)
: PnLTable()));
2021-08-09 07:13:42 -07:00
})
]);
}
}
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:
2021-09-14 07:29:40 -07:00
return pnl.price;
2021-08-09 07:13:42 -07:00
case 1:
2021-09-14 07:29:40 -07:00
return pnl.realized;
2021-08-09 07:13:42 -07:00
case 2:
2021-09-14 07:29:40 -07:00
return pnl.unrealized;
2021-08-09 07:13:42 -07:00
case 3:
2021-09-14 07:29:40 -07:00
return pnl.realized + pnl.unrealized;
2021-08-09 07:13:42 -07:00
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(
2021-09-11 19:27:31 -07:00
label: Text(S.of(context).date + 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")),
2021-09-21 05:52:52 -07:00
DataCell(Text(decimalFormat(pnl.amount, 2))),
DataCell(Text(decimalFormat(pnl.price, 3))),
DataCell(Text(decimalFormat(pnl.realized, 3))),
DataCell(Text(decimalFormat(pnl.unrealized, 3))),
DataCell(Text(decimalFormat(pnl.realized + pnl.unrealized, 3))),
2021-08-09 07:13:42 -07:00
]);
}
@override
bool get isRowCountApproximate => false;
@override
int get rowCount => accountManager.pnls.length;
@override
int get selectedRowCount => 0;
}