Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(webapp): add leverage slider #2178

Merged
merged 2 commits into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
153 changes: 86 additions & 67 deletions webapp/frontend/lib/common/scaffold_with_nav.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import 'dart:async';

import 'package:decimal/decimal.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get_10101/auth/auth_service.dart';
import 'package:get_10101/auth/login_screen.dart';
import 'package:get_10101/common/amount_text.dart';
import 'package:get_10101/common/balance.dart';
import 'package:get_10101/common/color.dart';
import 'package:get_10101/common/currency_change_notifier.dart';
import 'package:get_10101/common/currency_selection_widget.dart';
import 'package:get_10101/common/model.dart';
Expand Down Expand Up @@ -235,75 +237,92 @@ class ScaffoldWithNavigationRail extends StatelessWidget {
padding: const EdgeInsets.all(25),
child: Row(
children: [
Row(
children: [
TopBarItem(
label: 'Latest Bid: ',
value: bestQuote?.bid == null
? []
: [
TextSpan(
text: bestQuote?.bid?.toString(),
style: const TextStyle(fontWeight: FontWeight.bold),
)
]),
const SizedBox(width: 30),
TopBarItem(
label: 'Latest Ask: ',
value: bestQuote?.ask == null
? []
: [
TextSpan(
text: bestQuote?.ask?.toString(),
style: const TextStyle(fontWeight: FontWeight.bold),
)
]),
const SizedBox(width: 30),
TopBarItem(
label: 'Off-chain: ',
value: balance == null
? []
: [
formatAmountAsCurrency(
balance?.offChain, currency, midMarket),
]),
const SizedBox(width: 30),
TopBarItem(
label: 'On-chain: ',
value: balance == null
? []
: [
formatAmountAsCurrency(balance?.onChain, currency, midMarket),
]),
const SizedBox(width: 30),
TopBarItem(
label: 'Total: ',
value: balance == null
? []
: [
formatAmountAsCurrency(
balance?.onChain.add(balance?.offChain ?? Amount.zero()),
currency,
midMarket),
]),
],
),
Expanded(
child: Row(mainAxisAlignment: MainAxisAlignment.end, children: [
ElevatedButton(
onPressed: () {
context
.read<AuthService>()
.signOut()
.then((value) => GoRouter.of(context).go(LoginScreen.route))
.catchError((error) {
final messenger = ScaffoldMessenger.of(context);
showSnackBar(messenger, error);
});
},
child: const Text("Sign out"))
]),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
TopBarItem(
label: 'Latest Bid: ',
value: bestQuote?.bid == null
? []
: [
TextSpan(
text: bestQuote?.bid?.toString(),
style: const TextStyle(fontWeight: FontWeight.bold),
)
]),
const SizedBox(width: 30),
TopBarItem(
label: 'Latest Ask: ',
value: bestQuote?.ask == null
? []
: [
TextSpan(
text: bestQuote?.ask?.toString(),
style: const TextStyle(fontWeight: FontWeight.bold),
)
]),
const SizedBox(width: 30),
TopBarItem(
label: 'Off-chain: ',
value: balance == null
? []
: [
formatAmountAsCurrency(
balance?.offChain, currency, midMarket),
]),
const SizedBox(width: 30),
TopBarItem(
label: 'On-chain: ',
value: balance == null
? []
: [
formatAmountAsCurrency(
balance?.onChain, currency, midMarket),
]),
const SizedBox(width: 30),
TopBarItem(
label: 'Total: ',
value: balance == null
? []
: [
formatAmountAsCurrency(
balance?.onChain
.add(balance?.offChain ?? Amount.zero()),
currency,
midMarket),
]),
],
),
),
),
const SizedBox(width: 10),
IconButton(
onPressed: () {
context
.read<AuthService>()
.signOut()
.then((value) => GoRouter.of(context).go(LoginScreen.route))
.catchError((error) {
final messenger = ScaffoldMessenger.of(context);
showSnackBar(messenger, error);
});
},
icon: const CircleAvatar(
backgroundColor: tenTenOnePurple,
child: Icon(
FontAwesomeIcons.arrowRightFromBracket,
color: Colors.white,
size: 14,
),
),
color: Colors.white,
iconSize: 16,
padding: const EdgeInsets.all(4),
splashRadius: 15,
constraints: const BoxConstraints(),
)
],
),
),
Expand Down
175 changes: 175 additions & 0 deletions webapp/frontend/lib/trade/leverage_slider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get_10101/common/color.dart';
import 'package:get_10101/common/theme.dart';
import 'package:intl/intl.dart';
import 'package:syncfusion_flutter_sliders/sliders.dart';
import 'package:syncfusion_flutter_core/theme.dart' as slider_theme;

const gradientColors = <Color>[Colors.green, Colors.deepOrange];

const LinearGradient gradient = LinearGradient(colors: gradientColors);

const double minLeverage = 1.0;
const double maxLeverage = 5.0;

/// Slider that allows the user to select a leverage between minLeverage and maxLeverage.
/// It uses linear scale and fractional leverage values are rounded to the nearest integer.
class LeverageSlider extends StatefulWidget {
final double initialValue;
final Function(double) onLeverageChanged;

const LeverageSlider({required this.onLeverageChanged, this.initialValue = 2, super.key});

@override
State<LeverageSlider> createState() => _LeverageSliderState();
}

class _LeverageSliderState extends State<LeverageSlider> {
late double _leverage;

@override
void initState() {
_leverage = widget.initialValue;
super.initState();
}

@override
Widget build(BuildContext context) {
return InputDecorator(
decoration: InputDecoration(
border: const OutlineInputBorder(),
labelText: "Leverage",
labelStyle: const TextStyle(color: tenTenOnePurple),
filled: true,
fillColor: Colors.white,
errorStyle: TextStyle(
color: Colors.red[900],
),
),
child: Padding(
padding: const EdgeInsets.only(left: 8, right: 8),
child: SizedBox(
height: 35,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
RoundedIconButton(
icon: FontAwesomeIcons.minus,
onTap: () {
setState(() {
updateLeverage(_leverage > 1 ? _leverage - 1.0 : _leverage);
});
},
),
Expanded(
child: Padding(
padding: const EdgeInsets.only(left: 2, right: 2),
child: slider_theme.SfSliderTheme(
data: slider_theme.SfSliderThemeData(
activeLabelStyle: const TextStyle(color: Colors.black, fontSize: 12),
inactiveLabelStyle: const TextStyle(color: Colors.black45, fontSize: 12),
activeTrackColor: tenTenOnePurple.shade50,
inactiveTrackColor: tenTenOnePurple.shade50,
),
child: SfSlider(
min: 1,
max: maxLeverage,
value: _leverage,
stepSize: 1,
interval: 1,
showTicks: true,
showLabels: true,
enableTooltip: true,
tooltipShape: const SfPaddleTooltipShape(),
numberFormat: NumberFormat("x"),
tooltipTextFormatterCallback: (dynamic actualValue, String formattedText) {
return "${actualValue}x";
},
onChanged: (dynamic value) {
updateLeverage(value);
},
),
),
),
),
RoundedIconButton(
icon: FontAwesomeIcons.plus,
onTap: () {
updateLeverage(_leverage < maxLeverage ? _leverage + 1.0 : maxLeverage);
},
),
],
),
),
),
);
}

updateLeverage(double leverage) {
setState(() {
_leverage = leverage;
});

widget.onLeverageChanged(_leverage);
}
}

class LeverageButton extends StatelessWidget {
const LeverageButton({required this.label, required this.onPressed, super.key});

final Function onPressed;
final String label;

@override
Widget build(BuildContext context) {
TenTenOneTheme tradeTheme = Theme.of(context).extension<TenTenOneTheme>()!;

return SizedBox(
width: 30,
height: 30,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
padding: EdgeInsets.zero,
backgroundColor: tradeTheme.leverageMinusButtonColor),
onPressed: () => onPressed(),
child: Text(
label,
style: const TextStyle(color: Colors.white),
)),
);
}
}

class RoundedIconButton extends StatelessWidget {
final IconData icon;
final VoidCallback onTap;

const RoundedIconButton({
Key? key,
required this.icon,
required this.onTap,
}) : super(key: key);

@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
child: Container(
width: 20,
height: 20,
decoration: BoxDecoration(
shape: BoxShape.rectangle,
color: tenTenOnePurple,
borderRadius: BorderRadius.circular(3),
),
child: Icon(
icon,
color: Colors.white,
size: 16,
),
),
);
}
}
12 changes: 5 additions & 7 deletions webapp/frontend/lib/trade/trade_screen_order_form.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import 'package:get_10101/common/direction.dart';
import 'package:get_10101/common/model.dart';
import 'package:get_10101/common/theme.dart';
import 'package:get_10101/trade/create_order_confirmation_dialog.dart';
import 'package:get_10101/trade/leverage_slider.dart';
import 'package:get_10101/trade/quote_change_notifier.dart';
import 'package:get_10101/trade/quote_service.dart';
import 'package:provider/provider.dart';
Expand Down Expand Up @@ -70,15 +71,12 @@ class _NewOrderForm extends State<NewOrderForm> {
spaceBetweenRows,
Align(
alignment: AlignmentDirectional.centerEnd,
child: AmountInputField(
value: _leverage,
enabled: true,
label: "Leverage",
textAlign: TextAlign.right,
onChanged: (leverage) => setState(() {
_leverage = Leverage(double.parse(leverage));
child: LeverageSlider(
onLeverageChanged: (leverage) => setState(() {
_leverage = Leverage(leverage);
updateOrderValues();
}),
initialValue: _leverage.asDouble,
),
),
spaceBetweenRows,
Expand Down
2 changes: 2 additions & 0 deletions webapp/frontend/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ dependencies:
flutter_inappwebview: ^6.0.0-beta.23
json_annotation: ^4.8.1
url_launcher: ^6.2.4
syncfusion_flutter_sliders: ^24.2.9
syncfusion_flutter_core: ^24.2.9

dev_dependencies:
build_runner: ^2.4.8
Expand Down