Skip to content

Commit

Permalink
Merge pull request #2178 from get10101/feat/leverage-slider
Browse files Browse the repository at this point in the history
feat(webapp): add leverage slider
  • Loading branch information
bonomat committed Mar 7, 2024
2 parents 61867e0 + a7a0fc3 commit e47519f
Show file tree
Hide file tree
Showing 4 changed files with 268 additions and 74 deletions.
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

0 comments on commit e47519f

Please sign in to comment.