dev
dart 315 lines 9.93 KB
Raw
1 import 'package:cake_wallet/entities/fiat_currency.dart';
2 import 'package:cake_wallet/generated/i18n.dart';
3 import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_list_container.dart';
4 import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_search_field.dart';
5 import 'package:cake_wallet/new-ui/widgets/currency_picker/fiat_currency_row.dart';
6 import 'package:cake_wallet/new-ui/widgets/currency_picker/fiat_currency_search_result.dart';
7 import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
8 import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
9 import 'package:cw_core/crypto_currency.dart';
10 import 'package:flutter/material.dart';
11
12 class FiatCurrencyPickerSheet extends StatefulWidget {
13 const FiatCurrencyPickerSheet({
14 super.key,
15 required this.selected,
16 required this.onSelected,
17 this.cryptoOption,
18 this.onCryptoSelected,
19 });
20
21 final Object? selected;
22 final ValueChanged<FiatCurrency> onSelected;
23 final CryptoCurrency? cryptoOption;
24 final ValueChanged<CryptoCurrency>? onCryptoSelected;
25
26 static const _popularOrder = <FiatCurrency>[
27 FiatCurrency.usd,
28 FiatCurrency.eur,
29 FiatCurrency.gbp,
30 FiatCurrency.jpy,
31 FiatCurrency.cny,
32 ];
33
34 static Future<void> show({
35 required BuildContext context,
36 required Object? selected,
37 required ValueChanged<FiatCurrency> onSelected,
38 CryptoCurrency? cryptoOption,
39 ValueChanged<CryptoCurrency>? onCryptoSelected,
40 }) {
41 return showModalBottomSheet<void>(
42 context: context,
43 isScrollControlled: true,
44 useSafeArea: true,
45 enableDrag: false,
46 backgroundColor: Colors.transparent,
47 builder: (_) => FiatCurrencyPickerSheet(
48 selected: selected,
49 onSelected: onSelected,
50 cryptoOption: cryptoOption,
51 onCryptoSelected: onCryptoSelected,
52 ),
53 );
54 }
55
56 @override
57 State<FiatCurrencyPickerSheet> createState() => _FiatCurrencyPickerSheetState();
58 }
59
60 class _FiatCurrencyPickerSheetState extends State<FiatCurrencyPickerSheet> {
61 final TextEditingController _searchController = TextEditingController();
62 late final List<FiatCurrency> _allSorted = [...FiatCurrency.all]..sort(
63 (a, b) => a.fullName.toLowerCase().compareTo(b.fullName.toLowerCase()),
64 );
65
66 bool get _isSearching => _searchController.text.trim().isNotEmpty;
67
68 @override
69 void initState() {
70 super.initState();
71 _searchController.addListener(() => setState(() {}));
72 }
73
74 @override
75 void dispose() {
76 _searchController.dispose();
77 super.dispose();
78 }
79
80 bool _matches(FiatCurrency c, String query) {
81 if (query.isEmpty) return true;
82 final q = query.toLowerCase();
83 return c.fullName.toLowerCase().contains(q) || c.title.toLowerCase().contains(q);
84 }
85
86 void _selectFiatCurrency(FiatCurrency c) {
87 widget.onSelected(c);
88 Navigator.of(context).maybePop();
89 }
90
91 void _selectCryptoCurrency(CryptoCurrency c) {
92 widget.onCryptoSelected?.call(c);
93 Navigator.of(context).maybePop();
94 }
95
96 @override
97 Widget build(BuildContext context) {
98 final colors = Theme.of(context).colorScheme;
99 final selected = widget.selected;
100 final selectedFiat = selected is FiatCurrency ? selected : null;
101 final popular = FiatCurrencyPickerSheet._popularOrder
102 .where((c) => c != selectedFiat)
103 .toList(growable: false);
104 final query = _searchController.text.trim();
105 final filteredAll = _allSorted.where((c) => _matches(c, query)).toList(growable: false);
106
107 return Padding(
108 padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
109 child: Container(
110 decoration: BoxDecoration(
111 borderRadius: const BorderRadius.vertical(top: Radius.circular(24)),
112 color: colors.surface,
113 ),
114 child: Column(
115 mainAxisSize: MainAxisSize.max,
116 children: [
117 ModalTopBar(
118 title: S.of(context).select_fiat_currency_title,
119 leadingIcon: const Icon(Icons.close),
120 leadingSemanticLabel: S.of(context).close,
121 onLeadingPressed: () => Navigator.of(context).maybePop(),
122 ),
123 Expanded(
124 child: _isSearching
125 ? FiatCurrencySearchResults(
126 items: filteredAll,
127 onSelected: _selectFiatCurrency,
128 selected: selectedFiat,
129 )
130 : FiatCurrencyPickerBody(
131 selected: selected,
132 cryptoOption: widget.cryptoOption,
133 popular: popular,
134 all: filteredAll,
135 onSelectedFiat: _selectFiatCurrency,
136 onSelectedCrypto: _selectCryptoCurrency,
137 ),
138 ),
139 CurrencyPickerSearchField(
140 controller: _searchController,
141 hintText: S.of(context).search,
142 ),
143 const SizedBox(height: 24),
144 ],
145 ),
146 ),
147 );
148 }
149 }
150
151 class FiatCurrencyPickerBody extends StatelessWidget {
152 const FiatCurrencyPickerBody({
153 super.key,
154 required this.selected,
155 required this.cryptoOption,
156 required this.popular,
157 required this.all,
158 required this.onSelectedFiat,
159 required this.onSelectedCrypto,
160 });
161
162 final Object? selected;
163 final CryptoCurrency? cryptoOption;
164 final List<FiatCurrency> popular;
165 final List<FiatCurrency> all;
166 final void Function(FiatCurrency) onSelectedFiat;
167 final void Function(CryptoCurrency) onSelectedCrypto;
168
169 @override
170 Widget build(BuildContext context) {
171 final selectedFiat = selected is FiatCurrency ? selected as FiatCurrency : null;
172 final selectedCrypto = selected is CryptoCurrency ? selected as CryptoCurrency : null;
173 final showCryptoSection = cryptoOption != null && selectedCrypto == null;
174
175 return ListView(
176 padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
177 children: [
178 if (selectedFiat != null || selectedCrypto != null) ...[
179 _SectionHeader(label: S.of(context).picker_section_selected),
180 CurrencyPickerListContainer(
181 rows: [
182 if (selectedFiat != null)
183 FiatCurrencyRow(
184 currency: selectedFiat,
185 isSelected: true,
186 onTap: () => onSelectedFiat(selectedFiat),
187 ),
188 if (selectedCrypto != null)
189 _CryptoOptionRow(
190 currency: selectedCrypto,
191 isSelected: true,
192 onTap: () => onSelectedCrypto(selectedCrypto),
193 ),
194 ],
195 ),
196 const SizedBox(height: 16),
197 ],
198 if (showCryptoSection) ...[
199 _SectionHeader(label: S.of(context).picker_section_crypto),
200 CurrencyPickerListContainer(
201 rows: [
202 _CryptoOptionRow(
203 currency: cryptoOption!,
204 isSelected: false,
205 onTap: () => onSelectedCrypto(cryptoOption!),
206 ),
207 ],
208 ),
209 const SizedBox(height: 16),
210 ],
211 if (popular.isNotEmpty) ...[
212 _SectionHeader(label: S.of(context).picker_section_popular),
213 CurrencyPickerListContainer(
214 rows: [
215 for (final c in popular)
216 FiatCurrencyRow(
217 currency: c,
218 isSelected: false,
219 onTap: () => onSelectedFiat(c),
220 ),
221 ],
222 ),
223 const SizedBox(height: 16),
224 ],
225 _SectionHeader(label: S.of(context).picker_section_all_currencies),
226 CurrencyPickerListContainer(
227 rows: [
228 for (final c in all)
229 FiatCurrencyRow(
230 currency: c,
231 isSelected: c == selectedFiat,
232 onTap: () => onSelectedFiat(c),
233 ),
234 ],
235 ),
236 ],
237 );
238 }
239 }
240
241 class _SectionHeader extends StatelessWidget {
242 const _SectionHeader({required this.label});
243
244 final String label;
245
246 @override
247 Widget build(BuildContext context) {
248 return Padding(
249 padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 8),
250 child: Text(
251 label,
252 style: Theme.of(context).textTheme.bodyMedium?.copyWith(
253 fontWeight: FontWeight.w500,
254 color: Theme.of(context).colorScheme.onSurfaceVariant,
255 ),
256 ),
257 );
258 }
259 }
260
261 class _CryptoOptionRow extends StatelessWidget {
262 const _CryptoOptionRow({
263 required this.currency,
264 required this.isSelected,
265 required this.onTap,
266 });
267
268 final CryptoCurrency currency;
269 final bool isSelected;
270 final VoidCallback onTap;
271
272 @override
273 Widget build(BuildContext context) {
274 final colors = Theme.of(context).colorScheme;
275 return InkWell(
276 onTap: onTap,
277 child: Padding(
278 padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12),
279 child: Row(
280 children: [
281 CakeImageWidget(
282 imageUrl: currency.iconPath,
283 width: 28,
284 height: 28,
285 fit: BoxFit.cover,
286 ),
287 const SizedBox(width: 12),
288 Expanded(
289 child: Column(
290 crossAxisAlignment: CrossAxisAlignment.start,
291 mainAxisSize: MainAxisSize.min,
292 children: [
293 Text(
294 currency.fullName ?? currency.title,
295 style: Theme.of(context).textTheme.bodyMedium?.copyWith(
296 fontWeight: FontWeight.w500,
297 ),
298 ),
299 const SizedBox(height: 2),
300 Text(
301 currency.title,
302 style: Theme.of(context).textTheme.bodySmall?.copyWith(
303 color: colors.onSurfaceVariant,
304 ),
305 ),
306 ],
307 ),
308 ),
309 if (isSelected) Icon(Icons.check, size: 20, color: colors.primary),
310 ],
311 ),
312 ),
313 );
314 }
315 }