dev
dart 372 lines 15 KB
Raw
1 import 'package:cake_wallet/generated/i18n.dart';
2 import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
3 import 'package:cw_core/card_design.dart';
4 import 'package:flutter/material.dart';
5
6 class BalanceCardAction {
7 final String label;
8 final IconData icon;
9 final VoidCallback onTap;
10 final double? iconSize;
11
12 const BalanceCardAction(
13 {required this.label, required this.icon, required this.onTap, this.iconSize = 16});
14 }
15
16 class BalanceCard extends StatelessWidget {
17 const BalanceCard(
18 {super.key,
19 required this.width,
20 required this.design,
21 this.gradient,
22 this.borderRadius = 20,
23 this.selected = false,
24 this.accountName = "",
25 this.accountBalance = "",
26 this.balance = "",
27 this.fiatBalance = "",
28 this.assetName = "",
29 this.fiatCurrencyTitle = "",
30 this.designSwitchDuration = const Duration(),
31 this.actions = const [],
32 this.capitalizeAssetName = true,
33 this.onCustomizeTapped,
34 this.accountIndex,
35 this.fiatFirst = false});
36
37 final double width;
38 final double borderRadius;
39 final Gradient? gradient;
40 final String accountBalance;
41 final String accountName;
42 final String balance;
43 final String fiatBalance;
44 final String fiatCurrencyTitle;
45 final bool fiatFirst;
46 final int? accountIndex;
47 final bool capitalizeAssetName;
48 final String assetName;
49 final bool selected;
50 final CardDesign design;
51 final List<BalanceCardAction> actions;
52 final Duration designSwitchDuration;
53 final VoidCallback? onCustomizeTapped;
54
55 @override
56 Widget build(BuildContext context) {
57 final Duration textFadeDuration = Duration(milliseconds: 80);
58 final double iconWidth = width * 0.15;
59
60 final name = fiatFirst ? fiatCurrencyTitle : assetName;
61 final resolvedAssetName = capitalizeAssetName ? name.toUpperCase() : name.toLowerCase();
62
63 final leadText = fiatFirst ? S.of(context).wallet_balance : accountName;
64
65 final bool showText = accountBalance.isNotEmpty ||
66 leadText.isNotEmpty ||
67 balance.isNotEmpty ||
68 fiatBalance.isNotEmpty ||
69 resolvedAssetName.isNotEmpty;
70
71 final height = width * 0.62;
72
73 return AnimatedContainer(
74 duration: designSwitchDuration,
75 width: width,
76 height: height,
77 decoration: ShapeDecoration(
78 gradient: gradient ?? design.gradient,
79 shape: RoundedSuperellipseBorder(
80 side: const BorderSide(color: Color(0x44FFFFFF), width: 1),
81 borderRadius: BorderRadiusGeometry.circular(borderRadius),
82 ),
83 ),
84 child: Stack(
85 children: [
86 AnimatedSwitcher(
87 duration: designSwitchDuration,
88 switchInCurve: Curves.easeInOut,
89 switchOutCurve: Curves.easeInOut,
90 child: design.backgroundType == CardDesignBackgroundTypes.svgFull
91 ? ClipRSuperellipse(
92 borderRadius: BorderRadius.circular(borderRadius),
93 key: ValueKey(design.imagePath),
94 // Purely decorative card artwork.
95 child: ExcludeSemantics(
96 child: CakeImageWidget(
97 imageUrl: design.imagePath,
98 width: width,
99 height: height,
100 fit: BoxFit.fill,
101 ),
102 ),
103 )
104 : const SizedBox.shrink(
105 key: ValueKey('svgFullOff'),
106 ),
107 ),
108 Padding(
109 padding: EdgeInsets.all(width * 0.05),
110 child: Column(
111 crossAxisAlignment: CrossAxisAlignment.start,
112 mainAxisAlignment: MainAxisAlignment.spaceBetween,
113 mainAxisSize: MainAxisSize.max,
114 children: [
115 if (showText)
116 Column(
117 crossAxisAlignment: CrossAxisAlignment.start,
118 mainAxisSize: MainAxisSize.max,
119 children: [
120 if (leadText.isNotEmpty || accountBalance.isNotEmpty)
121 Row(
122 mainAxisSize: MainAxisSize.max,
123 mainAxisAlignment: MainAxisAlignment.spaceBetween,
124 children: [
125 Row(
126 spacing: 4,
127 children: [
128 if (accountIndex != null)
129 Opacity(
130 opacity: 0.5,
131 child: AnimatedDefaultTextStyle(
132 duration: designSwitchDuration,
133 style: Theme.of(context).textTheme.titleMedium!.copyWith(
134 fontWeight: FontWeight.w500,
135 color: design.colors.textColor),
136 child: Text("$accountIndex."),
137 ),
138 ),
139 AnimatedDefaultTextStyle(
140 duration: designSwitchDuration,
141 style: Theme.of(context).textTheme.titleMedium!.copyWith(
142 fontWeight: FontWeight.w500,
143 color: design.colors.textColor
144 .withAlpha(leadText == accountName ? 255 : 128)),
145 child: Text(leadText),
146 ),
147 ],
148 ),
149 AnimatedOpacity(
150 opacity: selected ? 0 : 1,
151 duration: textFadeDuration,
152 // Opacity alone keeps the text readable by screen
153 // readers, so drop it while it is invisible.
154 child: ExcludeSemantics(
155 excluding: selected,
156 child: Text(
157 accountBalance,
158 style: TextStyle(color: design.colors.textColor, fontSize: 14),
159 ),
160 ),
161 ),
162 ],
163 ),
164 AnimatedOpacity(
165 opacity: selected ? 1 : 0,
166 duration: textFadeDuration,
167 // Only the selected card's balance is visible, so only it
168 // may be announced.
169 child: ExcludeSemantics(
170 excluding: !selected,
171 child: AnimatedSwitcher(
172 duration: designSwitchDuration,
173 layoutBuilder: (currentChild, previousChildren) {
174 return Stack(
175 alignment: Alignment.centerLeft,
176 children: <Widget>[
177 ...previousChildren,
178 if (currentChild != null) currentChild,
179 ],
180 );
181 },
182 child: Row(
183 key: ValueKey("$balance ${resolvedAssetName.toUpperCase()}"),
184 spacing: 8.0,
185 children: [
186 AnimatedDefaultTextStyle(
187 duration: designSwitchDuration,
188 style: DefaultTextStyle.of(context).style.copyWith(
189 color: design.colors.textColor,
190 fontSize: 28,
191 fontWeight: FontWeight.w500,
192 letterSpacing: -0.4),
193 child: Text(fiatFirst ? fiatBalance : balance),
194 ),
195 AnimatedDefaultTextStyle(
196 duration: designSwitchDuration,
197 style: DefaultTextStyle.of(context).style.copyWith(
198 color: design.colors.textColorSecondary,
199 fontSize: 28,
200 fontWeight: FontWeight.w400,
201 letterSpacing: -0.4),
202 child: Text(resolvedAssetName),
203 ),
204 ],
205 ),
206 ),
207 ),
208 ),
209 AnimatedDefaultTextStyle(
210 duration: designSwitchDuration,
211 style: DefaultTextStyle.of(context).style.copyWith(
212 fontSize: 15,
213 fontWeight: FontWeight.w400,
214 color: design.colors.textColorSecondary),
215 child: AnimatedSwitcher(
216 duration: designSwitchDuration,
217 layoutBuilder: (currentChild, previousChildren) {
218 return Stack(
219 alignment: Alignment.centerLeft,
220 children: <Widget>[
221 ...previousChildren,
222 if (currentChild != null) currentChild,
223 ],
224 );
225 },
226 child: Text(
227 key: ValueKey(fiatFirst ? balance : fiatBalance),
228 fiatFirst ? "$assetName $balance" : fiatBalance,
229 ),
230 ),
231 ),
232 ],
233 )
234 else
235 Container(),
236 Row(
237 mainAxisSize: MainAxisSize.max,
238 mainAxisAlignment: MainAxisAlignment.spaceBetween,
239 crossAxisAlignment: CrossAxisAlignment.end,
240 children: [
241 AnimatedSwitcher(
242 duration: designSwitchDuration,
243 switchInCurve: Curves.easeInOut,
244 switchOutCurve: Curves.easeInOut,
245 layoutBuilder: (currentChild, previousChildren) {
246 return Stack(
247 alignment: Alignment.centerLeft,
248 children: <Widget>[
249 ...previousChildren,
250 if (currentChild != null) currentChild,
251 ],
252 );
253 },
254 child: Row(
255 key: ValueKey(actions.toString()),
256 crossAxisAlignment: CrossAxisAlignment.end,
257 mainAxisAlignment: MainAxisAlignment.start,
258 children: actions.map(getBalanceCardActionButton).toList(),
259 ),
260 ),
261 AnimatedSwitcher(
262 duration: designSwitchDuration,
263 switchInCurve: Curves.easeInOut,
264 switchOutCurve: Curves.easeInOut,
265 child: design.backgroundType == CardDesignBackgroundTypes.svgIcon
266 // Purely decorative card artwork.
267 ? ExcludeSemantics(
268 child: _CornerSvgIcon(design: design, iconWidth: iconWidth),
269 )
270 : const SizedBox.shrink(
271 key: ValueKey('svgIconOff'),
272 ),
273 ),
274 ],
275 ),
276 ],
277 ),
278 ),
279 Positioned(
280 right: 8,
281 top: 8,
282 child: AnimatedOpacity(
283 duration: designSwitchDuration,
284 opacity: onCustomizeTapped == null ? 0 : 1,
285 // Faded out and inert: must not be a focusable phantom control.
286 child: ExcludeSemantics(
287 excluding: onCustomizeTapped == null,
288 child: Semantics(
289 button: true,
290 label: S.of(context).wallet_menu,
291 onTap: onCustomizeTapped,
292 child: GestureDetector(
293 excludeFromSemantics: true,
294 behavior: HitTestBehavior.opaque,
295 onTap: onCustomizeTapped,
296 child: Container(
297 height: 40,
298 width: 40,
299 child: Center(
300 child: CakeImageWidget(
301 imageUrl: "assets/new-ui/3dots_vertical.svg",
302 alignment: Alignment.topRight,
303 colorFilter:
304 ColorFilter.mode(design.colors.textColorSecondary, BlendMode.srcIn),
305 ),
306 ),
307 ),
308 ),
309 ),
310 ),
311 ),
312 )
313 ],
314 ),
315 );
316 }
317
318 Widget getBalanceCardActionButton(BalanceCardAction action) => Semantics(
319 button: true,
320 label: action.label,
321 onTap: action.onTap,
322 child: ExcludeSemantics(
323 child: GestureDetector(
324 onTap: action.onTap,
325 child: Container(
326 decoration: BoxDecoration(
327 color: design.colors.backgroundImageColor.withAlpha(75),
328 borderRadius: BorderRadius.circular(10000000),
329 ),
330 margin: const EdgeInsets.only(right: 10),
331 padding: const EdgeInsets.only(left: 10, right: 5, top: 5, bottom: 5),
332 child: Row(
333 mainAxisSize: MainAxisSize.min,
334 children: [
335 Padding(
336 padding: const EdgeInsets.only(right: 6),
337 child: Text(
338 action.label,
339 style: TextStyle(color: design.colors.textColor, fontSize: 16),
340 ),
341 ),
342 Icon(action.icon, color: design.colors.textColorSecondary, size: action.iconSize),
343 ],
344 ),
345 ),
346 ),
347 ),
348 );
349 }
350
351 class _CornerSvgIcon extends StatelessWidget {
352 const _CornerSvgIcon({required this.design, required this.iconWidth});
353
354 final CardDesign design;
355 final double iconWidth;
356
357 @override
358 Widget build(BuildContext context) {
359 return CakeImageWidget(
360 imageUrl: design.imagePath,
361 key: ValueKey(design.imagePath),
362 height: iconWidth,
363 width: iconWidth,
364 colorFilter: design.preColoredIcon
365 ? null
366 : ColorFilter.mode(
367 design.colors.backgroundImageColor.withValues(alpha: 0.33),
368 BlendMode.dstIn,
369 ),
370 );
371 }
372 }