staking_screen.dart 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850
  1. import 'package:decimal/decimal.dart';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter_riverpod/flutter_riverpod.dart';
  4. import 'package:go_router/go_router.dart';
  5. import 'package:intl/intl.dart';
  6. import '../../../core/l10n/app_localizations.dart';
  7. import '../../../core/theme/app_colors.dart';
  8. import '../../../core/utils/top_toast.dart';
  9. import '../../../data/models/finance/staking_config.dart';
  10. import '../../../providers/auth_provider.dart';
  11. import '../../../providers/market_provider.dart';
  12. import '../../../providers/staking_provider.dart';
  13. import '../../widgets/finance/ido_overview_cards.dart';
  14. bool _financeIsDark(BuildContext context) =>
  15. Theme.of(context).brightness == Brightness.dark;
  16. Color _financePageBg(BuildContext context) => _financeIsDark(context)
  17. ? AppColors.darkBg
  18. : Theme.of(context).colorScheme.surface;
  19. Color _financePrimaryText(BuildContext context) => _financeIsDark(context)
  20. ? Colors.white
  21. : Theme.of(context).colorScheme.onSurface;
  22. Color _financeSecondaryText(BuildContext context) =>
  23. _financePrimaryText(context).withAlpha(160);
  24. Color _financeLabelText(BuildContext context) =>
  25. _financePrimaryText(context).withAlpha(140);
  26. Color _financeFieldBg(BuildContext context) =>
  27. _financePrimaryText(context).withAlpha(14);
  28. Color _financeFieldBorder(BuildContext context) =>
  29. _financePrimaryText(context).withAlpha(40);
  30. class StakingScreen extends ConsumerStatefulWidget {
  31. const StakingScreen({
  32. super.key,
  33. this.configId,
  34. this.showAppBar = true,
  35. });
  36. final String? configId;
  37. final bool showAppBar;
  38. @override
  39. ConsumerState<StakingScreen> createState() => _StakingScreenState();
  40. }
  41. class _StakingScreenState extends ConsumerState<StakingScreen> {
  42. final _amountController = TextEditingController();
  43. @override
  44. void initState() {
  45. super.initState();
  46. Future.microtask(() {
  47. ref.read(stakingProvider.notifier).init(configId: widget.configId);
  48. ref.read(marketProvider.notifier).loadSpotIfNeeded();
  49. });
  50. }
  51. @override
  52. void dispose() {
  53. _amountController.dispose();
  54. super.dispose();
  55. }
  56. @override
  57. Widget build(BuildContext context) {
  58. final l10n = AppLocalizations.of(context)!;
  59. final state = ref.watch(stakingProvider);
  60. final config = state.selectedConfig;
  61. final isLoggedIn = ref.watch(isLoggedInProvider);
  62. if (!state.isLoadingConfig && config == null) {
  63. final emptyBody = _EmptyState(
  64. message: state.errorMessage ?? l10n.financeConfigNotFound,
  65. );
  66. if (!widget.showAppBar) {
  67. return ColoredBox(
  68. color: _financePageBg(context),
  69. child: emptyBody,
  70. );
  71. }
  72. return Scaffold(
  73. backgroundColor: _financePageBg(context),
  74. appBar: AppBar(
  75. backgroundColor: Colors.transparent,
  76. foregroundColor: _financePrimaryText(context),
  77. elevation: 0,
  78. title: Text(
  79. l10n.financeIdoTitle,
  80. style: TextStyle(color: _financePrimaryText(context)),
  81. ),
  82. centerTitle: true,
  83. ),
  84. body: emptyBody,
  85. );
  86. }
  87. final coinUnit = config?.coinUnit ?? 'IBIT';
  88. final symbol = '${coinUnit.toUpperCase()}USDT';
  89. final ticker = ref.watch(spotTickerProvider(symbol));
  90. final unitPrice = _usdtPrice(coinUnit, ticker?.lastPrice ?? 0);
  91. final rulesText = config == null ? '' : _rulesText(l10n, config);
  92. final displayUnit = _displayCoinUnit(coinUnit);
  93. final unlockText =
  94. config == null ? '—' : _formatUnlockDate(_estimatedUnlockDate(config));
  95. final pageBody = ListView(
  96. padding: const EdgeInsets.only(bottom: 32),
  97. children: [
  98. _HeroSection(
  99. l10n: l10n,
  100. rulesText: rulesText,
  101. loadingRules: state.isLoadingConfig,
  102. ),
  103. IdoOverviewCards(
  104. teamNum: state.teamNum,
  105. lockedTotal: state.lockedTotal,
  106. loading: state.isLoadingOverview,
  107. ),
  108. Padding(
  109. padding: const EdgeInsets.fromLTRB(16, 0, 16, 0),
  110. child: Column(
  111. crossAxisAlignment: CrossAxisAlignment.start,
  112. children: [
  113. Row(
  114. crossAxisAlignment:CrossAxisAlignment.center,
  115. mainAxisAlignment:MainAxisAlignment.center,
  116. children: [
  117. Image.asset("assets/images/ico_airdrop_left.png",height:10,),
  118. SizedBox(width:10,),
  119. Padding(padding:EdgeInsetsGeometry.only(bottom:5),child:Text(
  120. l10n.financeJoinPresale,
  121. style: TextStyle(
  122. color: _financePrimaryText(context),
  123. fontSize: 16,
  124. fontWeight: FontWeight.w600,
  125. ),
  126. ),),
  127. SizedBox(width:10,),
  128. Image.asset("assets/images/ico_airdrop_right.png",height:10,),
  129. ],
  130. ),
  131. const SizedBox(height: 24),
  132. _LabelRow(
  133. label: l10n.financeSubscribeQty,
  134. actionLabel: l10n.goSpotTrade,
  135. onAction: config == null ? null : () => _goToSpotTrade(config),
  136. ),
  137. const SizedBox(height: 12),
  138. _LabelRow(label: l10n.financeCorrespondingPrice),
  139. const SizedBox(height: 12),
  140. _AvailableMetaRow(
  141. l10n: l10n,
  142. amount: _fmtAmount(state.fundingAvailable),
  143. unit: displayUnit,
  144. loading: state.isRefreshingBalance,
  145. onFillAll: state.isRefreshingBalance
  146. ? null
  147. : () => _fillAllAmount(state.fundingAvailable),
  148. ),
  149. const SizedBox(height: 12),
  150. _FieldInput(
  151. controller: _amountController,
  152. suffix: displayUnit,
  153. onChanged: (_) => setState(() {}),
  154. ),
  155. const SizedBox(height: 12),
  156. Row(
  157. children: [
  158. Expanded(
  159. child: _FieldInput(
  160. readOnly: true,
  161. value: _priceUsdtText(unitPrice),
  162. suffix: 'USDT',
  163. ),
  164. ),
  165. const SizedBox(width: 12),
  166. _TransferButton(
  167. label: l10n.stakingTransfer,
  168. onPressed: () => _openSpotFundTransfer(isLoggedIn),
  169. ),
  170. ],
  171. ),
  172. const SizedBox(height: 20),
  173. Row(
  174. mainAxisAlignment: MainAxisAlignment.center,
  175. crossAxisAlignment: CrossAxisAlignment.center,
  176. children: [
  177. _UnitPriceMetaRow(
  178. l10n: l10n,
  179. unitPrice: unitPrice,
  180. ),
  181. ],
  182. ),
  183. const SizedBox(height: 20),
  184. // const SizedBox(height: 32),
  185. Center(
  186. child: ConstrainedBox(
  187. constraints: const BoxConstraints(maxWidth: 360),
  188. child: SizedBox(
  189. width: double.infinity,
  190. height: 48,
  191. child: ElevatedButton(
  192. onPressed: state.isSubmitting ||
  193. config == null ||
  194. state.isLoadingConfig
  195. ? null
  196. : () => _submitStake(isLoggedIn),
  197. style: ElevatedButton.styleFrom(
  198. backgroundColor: AppColors.brand,
  199. foregroundColor: Colors.black,
  200. disabledBackgroundColor: AppColors.brand.withAlpha(128),
  201. shape: const StadiumBorder(),
  202. elevation: 0,
  203. ),
  204. child: state.isSubmitting
  205. ? const SizedBox(
  206. width: 18,
  207. height: 18,
  208. child: CircularProgressIndicator(
  209. strokeWidth: 2,
  210. ),
  211. )
  212. : Text(
  213. l10n.financeConfirmPresale,
  214. style: const TextStyle(
  215. fontSize: 16,
  216. fontWeight: FontWeight.w600,
  217. ),
  218. ),
  219. ),
  220. ),
  221. ),
  222. ),
  223. const SizedBox(height: 20),
  224. Row(
  225. crossAxisAlignment:CrossAxisAlignment.center,
  226. mainAxisAlignment:MainAxisAlignment.center,
  227. children: [
  228. Text(
  229. state.isLoadingConfig
  230. ? l10n.financeEstimatedUnlockLine('—')
  231. : l10n.financeEstimatedUnlockLine(unlockText),
  232. textAlign: TextAlign.center,
  233. style: TextStyle(
  234. color: _financeSecondaryText(context),
  235. fontSize: 13,
  236. ),
  237. ),
  238. ],
  239. ),
  240. if (!isLoggedIn) ...[
  241. const SizedBox(height: 12),
  242. Wrap(
  243. alignment: WrapAlignment.center,
  244. crossAxisAlignment: WrapCrossAlignment.center,
  245. children: [
  246. Text(
  247. l10n.financeLoginToStake,
  248. style: TextStyle(
  249. color: _financeSecondaryText(context),
  250. fontSize: 12,
  251. ),
  252. ),
  253. GestureDetector(
  254. onTap: () => context.push(
  255. '/login?redirect=${Uri.encodeComponent('/finance/ido')}',
  256. ),
  257. child: Text(
  258. l10n.login,
  259. style: const TextStyle(
  260. color: AppColors.brand,
  261. fontSize: 12,
  262. ),
  263. ),
  264. ),
  265. ],
  266. ),
  267. ],
  268. ],
  269. ),
  270. ),
  271. ],
  272. );
  273. if (!widget.showAppBar) {
  274. return ColoredBox(
  275. color: _financePageBg(context),
  276. child: pageBody,
  277. );
  278. }
  279. return Scaffold(
  280. backgroundColor: _financePageBg(context),
  281. appBar: AppBar(
  282. backgroundColor: Colors.transparent,
  283. foregroundColor: _financePrimaryText(context),
  284. elevation: 0,
  285. title: Text(
  286. l10n.financeIdoTitle,
  287. style: TextStyle(color: _financePrimaryText(context)),
  288. ),
  289. centerTitle: true,
  290. ),
  291. body: pageBody,
  292. );
  293. }
  294. Decimal _usdtPrice(String coinUnit, double wsPrice) {
  295. if (coinUnit.toUpperCase() == 'USDT') {
  296. return Decimal.one;
  297. }
  298. return Decimal.tryParse(wsPrice.toString()) ?? Decimal.zero;
  299. }
  300. String _priceUsdtText(Decimal unitPrice) {
  301. final qty = Decimal.tryParse(_amountController.text) ?? Decimal.zero;
  302. if (qty <= Decimal.zero || unitPrice <= Decimal.zero) {
  303. return '0.00';
  304. }
  305. final total = qty * unitPrice;
  306. return NumberFormat('#,##0.00', 'en_US').format(total.toDouble());
  307. }
  308. int _lockMonths(StakingConfig config) {
  309. final days = config.lockDays;
  310. return days <= 0 ? 1 : (days / 30).ceil().clamp(1, 999999);
  311. }
  312. int _releaseMonths(StakingConfig config) {
  313. if (config.releaseType != 1) {
  314. return 0;
  315. }
  316. final rate = double.tryParse(config.releaseRate) ?? 0;
  317. final periodDays = config.releasePeriod <= 0 ? 1 : config.releasePeriod;
  318. if (rate <= 0) {
  319. return (periodDays / 30).ceil().clamp(1, 999999);
  320. }
  321. final batches = (1 / rate).ceil();
  322. final totalDays = batches * periodDays;
  323. return (totalDays / 30).ceil().clamp(1, 999999);
  324. }
  325. String _rulesText(AppLocalizations l10n, StakingConfig config) {
  326. final lock = _lockMonths(config);
  327. if (config.releaseType == 1 && _releaseMonths(config) > 0) {
  328. return l10n.financeIdoRuleBatch('$lock', '${_releaseMonths(config)}');
  329. }
  330. return l10n.financeIdoRuleOnce('$lock');
  331. }
  332. String _displayCoinUnit(String coinUnit) {
  333. final unit = coinUnit.trim();
  334. if (unit.isEmpty) {
  335. return 'iBit';
  336. }
  337. if (unit.toUpperCase() == 'IBIT') {
  338. return 'iBit';
  339. }
  340. return unit;
  341. }
  342. String _fmtAmount(String value, {int maxFrac = 8}) {
  343. final parsed = double.tryParse(value);
  344. if (parsed == null || !parsed.isFinite) {
  345. return '0';
  346. }
  347. if (parsed == 0) {
  348. return '0';
  349. }
  350. return NumberFormat('#,##0.${'#' * maxFrac}', 'en_US').format(parsed);
  351. }
  352. DateTime _estimatedUnlockDate(StakingConfig config) {
  353. return DateTime.now().add(Duration(days: config.lockDays));
  354. }
  355. String _formatUnlockDate(DateTime date) {
  356. String pad(int n) {
  357. return n.toString().padLeft(2, '0');
  358. }
  359. return '${date.year}/${pad(date.month)}/${pad(date.day)} '
  360. '${pad(date.hour)}:${pad(date.minute)}:${pad(date.second)}';
  361. }
  362. void _fillAllAmount(String amount) {
  363. final parsed = double.tryParse(amount);
  364. if (parsed != null && parsed > 0) {
  365. _amountController.text = amount;
  366. setState(() {});
  367. }
  368. }
  369. void _goToSpotTrade(StakingConfig config) {
  370. final symbol = '${config.coinUnit.toUpperCase()}USDT';
  371. context.push('/market/spot/$symbol');
  372. }
  373. Future<void> _openSpotFundTransfer(bool isLoggedIn) async {
  374. if (!isLoggedIn) {
  375. context.push('/login?redirect=${Uri.encodeComponent('/finance/ido')}');
  376. return;
  377. }
  378. await context.push(
  379. '/asset/transfer?from=SPOT&to=SPOT_TRADING&symbol=IBIT&preferSymbol=1&bridgeOnly=1',
  380. );
  381. if (!mounted) {
  382. return;
  383. }
  384. await ref.read(stakingProvider.notifier).refreshBalances();
  385. }
  386. Future<void> _submitStake(bool isLoggedIn) async {
  387. final l10n = AppLocalizations.of(context)!;
  388. if (!isLoggedIn) {
  389. context.push('/login?redirect=${Uri.encodeComponent('/finance/ido')}');
  390. return;
  391. }
  392. final amount = _amountController.text.trim();
  393. final err = await ref.read(stakingProvider.notifier).submitStake(amount);
  394. if (!mounted) {
  395. return;
  396. }
  397. if (err == null) {
  398. _amountController.clear();
  399. showTopToast(
  400. context,
  401. message: l10n.financeStakeSuccess,
  402. backgroundColor: AppColors.rise,
  403. );
  404. setState(() {});
  405. return;
  406. }
  407. showTopToast(context, message: _resolveStakeError(err, l10n));
  408. }
  409. String _resolveStakeError(String err, AppLocalizations l10n) {
  410. if (err == 'errNeedLogin') {
  411. return l10n.financeLoginToStake;
  412. }
  413. if (err == 'errEnterAmount') {
  414. return l10n.financeAmountRequired;
  415. }
  416. if (err == 'errExceedBalance') {
  417. return l10n.errExceedBalance;
  418. }
  419. if (err.startsWith('errAmountBelowMin:')) {
  420. return l10n.financeBelowMin(err.substring('errAmountBelowMin:'.length));
  421. }
  422. if (err.startsWith('errAmountAboveMax:')) {
  423. return l10n.financeAboveMax(err.substring('errAmountAboveMax:'.length));
  424. }
  425. return err;
  426. }
  427. }
  428. class _EmptyState extends StatelessWidget {
  429. const _EmptyState({required this.message});
  430. final String message;
  431. @override
  432. Widget build(BuildContext context) {
  433. final l10n = AppLocalizations.of(context)!;
  434. return Center(
  435. child: Padding(
  436. padding: const EdgeInsets.all(24),
  437. child: Column(
  438. mainAxisSize: MainAxisSize.min,
  439. children: [
  440. Text(
  441. message,
  442. textAlign: TextAlign.center,
  443. style: TextStyle(color: _financeSecondaryText(context)),
  444. ),
  445. const SizedBox(height: 16),
  446. TextButton(
  447. onPressed: () => context.go('/'),
  448. child: Text(l10n.home),
  449. ),
  450. ],
  451. ),
  452. ),
  453. );
  454. }
  455. }
  456. class _HeroSection extends StatelessWidget {
  457. const _HeroSection({
  458. required this.l10n,
  459. required this.rulesText,
  460. this.loadingRules = false,
  461. });
  462. final AppLocalizations l10n;
  463. final String rulesText;
  464. final bool loadingRules;
  465. @override
  466. Widget build(BuildContext context) {
  467. return Container(
  468. width: double.infinity,
  469. padding: const EdgeInsets.fromLTRB(16, 8, 16, 24),
  470. decoration: const BoxDecoration(
  471. gradient: LinearGradient(
  472. begin: Alignment.topCenter,
  473. end: Alignment.bottomCenter,
  474. colors: [
  475. Color(0x0AF0B90B),
  476. Colors.transparent,
  477. ],
  478. stops: [0.0, 0.7],
  479. ),
  480. ),
  481. child: Row(
  482. crossAxisAlignment: CrossAxisAlignment.start,
  483. children: [
  484. Expanded(
  485. child: Column(
  486. crossAxisAlignment: CrossAxisAlignment.start,
  487. children: [
  488. Text(
  489. l10n.financeIdoTitle,
  490. style: TextStyle(
  491. color: _financePrimaryText(context),
  492. fontSize: 40,
  493. fontWeight: FontWeight.w700,
  494. height: 1.1,
  495. letterSpacing: 0.8,
  496. ),
  497. ),
  498. const SizedBox(height: 16),
  499. if (loadingRules)
  500. Container(
  501. height: 40,
  502. width: double.infinity,
  503. decoration: BoxDecoration(
  504. color: _financePrimaryText(context).withAlpha(14),
  505. borderRadius: BorderRadius.circular(6),
  506. ),
  507. )
  508. else
  509. Text.rich(
  510. TextSpan(
  511. children: [
  512. TextSpan(
  513. text: l10n.financeIdoRuleLabel,
  514. style: TextStyle(color: _financeLabelText(context)),
  515. ),
  516. TextSpan(text: rulesText),
  517. ],
  518. ),
  519. style: TextStyle(
  520. color: _financePrimaryText(context).withAlpha(191),
  521. fontSize: 15,
  522. height: 1.75,
  523. ),
  524. ),
  525. ],
  526. ),
  527. ),
  528. const SizedBox(width: 12),
  529. Image.asset(
  530. 'assets/images/finance/staking_hero.png',
  531. width: 120,
  532. height: 120,
  533. fit: BoxFit.contain,
  534. ),
  535. ],
  536. ),
  537. );
  538. }
  539. }
  540. class _LabelRow extends StatelessWidget {
  541. const _LabelRow({
  542. required this.label,
  543. this.actionLabel,
  544. this.onAction,
  545. });
  546. final String label;
  547. final String? actionLabel;
  548. final VoidCallback? onAction;
  549. @override
  550. Widget build(BuildContext context) {
  551. return Row(
  552. children: [
  553. Text(
  554. label,
  555. style: TextStyle(
  556. color: _financeLabelText(context),
  557. fontSize: 14,
  558. ),
  559. ),
  560. if (actionLabel != null && onAction != null) ...[
  561. const Spacer(),
  562. _LinkAction(label: actionLabel!, onTap: onAction!),
  563. ],
  564. ],
  565. );
  566. }
  567. }
  568. class _AvailableMetaRow extends StatelessWidget {
  569. const _AvailableMetaRow({
  570. required this.l10n,
  571. required this.amount,
  572. required this.unit,
  573. required this.onFillAll,
  574. this.loading = false,
  575. });
  576. final AppLocalizations l10n;
  577. final String amount;
  578. final String unit;
  579. final VoidCallback? onFillAll;
  580. final bool loading;
  581. @override
  582. Widget build(BuildContext context) {
  583. return Row(
  584. children: [
  585. Expanded(
  586. child: Text.rich(
  587. TextSpan(
  588. text: '${l10n.financeAvailableIbit}: ',
  589. style: TextStyle(
  590. color: _financeSecondaryText(context), fontSize: 13),
  591. children: [
  592. TextSpan(
  593. text: amount,
  594. style: const TextStyle(
  595. color: AppColors.brand,
  596. fontWeight: FontWeight.w600,
  597. ),
  598. ),
  599. TextSpan(text: ' $unit'),
  600. ],
  601. ),
  602. ),
  603. ),
  604. if (onFillAll != null)
  605. _LinkAction(label: l10n.all, onTap: onFillAll!)
  606. else if (loading)
  607. const SizedBox(
  608. width: 14,
  609. height: 14,
  610. child: CircularProgressIndicator(strokeWidth: 1.5),
  611. ),
  612. ],
  613. );
  614. }
  615. }
  616. class _UnitPriceMetaRow extends StatelessWidget {
  617. const _UnitPriceMetaRow({
  618. required this.l10n,
  619. required this.unitPrice,
  620. });
  621. final AppLocalizations l10n;
  622. final Decimal unitPrice;
  623. @override
  624. Widget build(BuildContext context) {
  625. if (unitPrice <= Decimal.zero) {
  626. return const SizedBox(height: 20);
  627. }
  628. return Text(
  629. l10n.financeIbitUnitPriceLine(
  630. NumberFormat('#,##0.0000', 'en_US').format(unitPrice.toDouble()),
  631. ),
  632. style: TextStyle(
  633. color: _financeIsDark(context)
  634. ? const Color(0xFFF0B90B)
  635. : _financeSecondaryText(context),
  636. fontSize: 13,
  637. ),
  638. );
  639. }
  640. }
  641. class _LinkAction extends StatelessWidget {
  642. const _LinkAction({required this.label, required this.onTap});
  643. final String label;
  644. final VoidCallback onTap;
  645. @override
  646. Widget build(BuildContext context) {
  647. return GestureDetector(
  648. onTap: onTap,
  649. child: Text(
  650. label,
  651. style: const TextStyle(
  652. color: AppColors.brand,
  653. fontSize: 13,
  654. ),
  655. ),
  656. );
  657. }
  658. }
  659. class _FieldInput extends StatelessWidget {
  660. const _FieldInput({
  661. this.controller,
  662. this.value,
  663. this.suffix,
  664. this.onChanged,
  665. this.readOnly = false,
  666. });
  667. final TextEditingController? controller;
  668. final String? value;
  669. final String? suffix;
  670. final ValueChanged<String>? onChanged;
  671. final bool readOnly;
  672. @override
  673. Widget build(BuildContext context) {
  674. final valueStyle = TextStyle(
  675. color: _financePrimaryText(context),
  676. fontSize: 16,
  677. height: 1.2,
  678. fontFeatures: const [FontFeature.tabularFigures()],
  679. );
  680. final inputDecoration = InputDecoration(
  681. border: InputBorder.none,
  682. enabledBorder: InputBorder.none,
  683. focusedBorder: InputBorder.none,
  684. disabledBorder: InputBorder.none,
  685. errorBorder: InputBorder.none,
  686. focusedErrorBorder: InputBorder.none,
  687. filled: false,
  688. isDense: true,
  689. isCollapsed: true,
  690. contentPadding: EdgeInsets.zero,
  691. hintText: '0',
  692. hintStyle: TextStyle(
  693. color: _financePrimaryText(context).withAlpha(64),
  694. fontSize: 16,
  695. height: 1.2,
  696. fontFeatures: const [FontFeature.tabularFigures()],
  697. ),
  698. );
  699. return Container(
  700. height: 52,
  701. padding: const EdgeInsets.symmetric(horizontal: 16),
  702. decoration: BoxDecoration(
  703. color: readOnly
  704. ? _financePrimaryText(context).withAlpha(8)
  705. : _financeFieldBg(context),
  706. border: Border.all(color: _financeFieldBorder(context)),
  707. borderRadius: BorderRadius.circular(12),
  708. ),
  709. child: Row(
  710. crossAxisAlignment: CrossAxisAlignment.center,
  711. children: [
  712. Expanded(
  713. child: readOnly
  714. ? Align(
  715. alignment: Alignment.centerLeft,
  716. child: Text(
  717. value ?? '0.00',
  718. maxLines: 1,
  719. overflow: TextOverflow.ellipsis,
  720. style: valueStyle,
  721. ),
  722. )
  723. : Theme(
  724. data: Theme.of(context).copyWith(
  725. inputDecorationTheme: const InputDecorationTheme(
  726. filled: false,
  727. fillColor: Colors.transparent,
  728. border: InputBorder.none,
  729. enabledBorder: InputBorder.none,
  730. focusedBorder: InputBorder.none,
  731. disabledBorder: InputBorder.none,
  732. errorBorder: InputBorder.none,
  733. focusedErrorBorder: InputBorder.none,
  734. contentPadding: EdgeInsets.zero,
  735. isDense: true,
  736. ),
  737. ),
  738. child: TextField(
  739. controller: controller,
  740. readOnly: readOnly,
  741. keyboardType: const TextInputType.numberWithOptions(
  742. decimal: true,
  743. ),
  744. style: valueStyle,
  745. cursorColor: AppColors.brand,
  746. decoration: inputDecoration,
  747. onChanged: onChanged,
  748. ),
  749. ),
  750. ),
  751. if (suffix != null) ...[
  752. const SizedBox(width: 12),
  753. Text(
  754. suffix!,
  755. style: TextStyle(
  756. color: _financeLabelText(context),
  757. fontSize: 14,
  758. ),
  759. ),
  760. ],
  761. ],
  762. ),
  763. );
  764. }
  765. }
  766. class _TransferButton extends StatelessWidget {
  767. const _TransferButton({required this.label, required this.onPressed});
  768. final String label;
  769. final VoidCallback onPressed;
  770. @override
  771. Widget build(BuildContext context) {
  772. return SizedBox(
  773. height: 52,
  774. child: OutlinedButton(
  775. onPressed: onPressed,
  776. style: OutlinedButton.styleFrom(
  777. side: const BorderSide(color: AppColors.brand),
  778. foregroundColor: AppColors.brand,
  779. shape: RoundedRectangleBorder(
  780. borderRadius: BorderRadius.circular(12),
  781. ),
  782. padding: const EdgeInsets.symmetric(horizontal: 20),
  783. ),
  784. child: Text(
  785. label,
  786. style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
  787. ),
  788. ),
  789. );
  790. }
  791. }