Cw 433 support send templates with multiple recipients (#995)
* feat: Support Send templates with multiple recipients * feat: use only first name for template display, and sum total amount * fix: amounts being wiped * feat: make send template card buttons function like send card * feat: replace amount -> name for template name * fix: template name
Rafael Saes committed
Aug 1, 2023 at 19:19 UTC
fcf4fbdc14d2aab9e8c9eda6662dde91c1d3ae7f
33 files changed
+706
-429
lib/entities/template.dart
+14
-9
@@ -4,14 +4,15 @@ part 'template.g.dart';
4
5
@HiveType(typeId: Template.typeId)
6
class Template extends HiveObject {
7
- Template({
8
- required this.nameRaw,
9
- required this.isCurrencySelectedRaw,
10
- required this.addressRaw,
11
- required this.cryptoCurrencyRaw,
12
- required this.amountRaw,
13
- required this.fiatCurrencyRaw,
14
- required this.amountFiatRaw});
7
+ Template(
8
+ {required this.nameRaw,
9
+ required this.isCurrencySelectedRaw,
10
+ required this.addressRaw,
11
+ required this.cryptoCurrencyRaw,
12
+ required this.amountRaw,
13
+ required this.fiatCurrencyRaw,
14
+ required this.amountFiatRaw,
15
+ this.additionalRecipientsRaw});
16
17
static const typeId = 6;
18
static const boxName = 'Template';
@@ -37,6 +38,9 @@ class Template extends HiveObject {
38
@HiveField(6)
39
String? amountFiatRaw;
40
41
+ @HiveField(7)
42
+ List<Template>? additionalRecipientsRaw;
43
+
44
bool get isCurrencySelected => isCurrencySelectedRaw ?? false;
45
46
String get fiatCurrency => fiatCurrencyRaw ?? '';
@@ -50,5 +54,6 @@ class Template extends HiveObject {
54
String get cryptoCurrency => cryptoCurrencyRaw ?? '';
55
56
String get amount => amountRaw ?? '';
53
-}
57
58
+ List<Template>? get additionalRecipients => additionalRecipientsRaw ?? null;
59
+}
lib/src/screens/send/send_page.dart
+52
-17
@@ -1,4 +1,5 @@
1
import 'package:cake_wallet/entities/fiat_currency.dart';
2
+import 'package:cake_wallet/entities/template.dart';
3
import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart';
4
import 'package:cake_wallet/src/screens/send/widgets/send_card.dart';
5
import 'package:cake_wallet/src/widgets/add_template_button.dart';
@@ -241,6 +242,11 @@ class SendPage extends BasePage {
242
return TemplateTile(
243
key: UniqueKey(),
244
to: template.name,
245
+ hasMultipleRecipients:
246
+ template.additionalRecipients !=
247
+ null &&
248
+ template.additionalRecipients!
249
+ .length > 1,
250
amount: template.isCurrencySelected
251
? template.amount
252
: template.amountFiat,
@@ -248,25 +254,36 @@ class SendPage extends BasePage {
254
? template.cryptoCurrency
255
: template.fiatCurrency,
256
onTap: () async {
251
- final fiatFromTemplate = FiatCurrency
252
- .all
253
- .singleWhere((element) =>
254
- element.title ==
255
- template.fiatCurrency);
256
- final output = _defineCurrentOutput();
257
- output.address = template.address;
258
- if (template.isCurrencySelected) {
259
- output
260
- .setCryptoAmount(template.amount);
257
+ if (template.additionalRecipients !=
258
+ null) {
259
+ sendViewModel.clearOutputs();
260
+
261
+ template.additionalRecipients!
262
+ .forEach((currentElement) async {
263
+ int i = template
264
+ .additionalRecipients!
265
+ .indexOf(currentElement);
266
+
267
+ Output output;
268
+ try {
269
+ output = sendViewModel.outputs[i];
270
+ } catch (e) {
271
+ sendViewModel.addOutput();
272
+ output = sendViewModel.outputs[i];
273
+ }
274
+
275
+ await _setInputsFromTemplate(
276
+ context,
277
+ output: output,
278
+ template: currentElement);
279
+ });
280
} else {
262
- sendViewModel.setFiatCurrency(
263
- fiatFromTemplate);
264
- output.setFiatAmount(
265
- template.amountFiat);
281
+ final output = _defineCurrentOutput();
282
+ await _setInputsFromTemplate(
283
+ context,
284
+ output: output,
285
+ template: template);
286
}
267
- output.resetParsedAddress();
268
- await output
269
- .fetchParsedAddress(context);
287
},
288
onRemove: () {
289
showPopUp<void>(
@@ -477,6 +494,24 @@ class SendPage extends BasePage {
494
_effectsInstalled = true;
495
}
496
497
+ Future<void> _setInputsFromTemplate(BuildContext context,
498
+ {required Output output, required Template template}) async {
499
+ final fiatFromTemplate = FiatCurrency.all
500
+ .singleWhere((element) => element.title == template.fiatCurrency);
501
+
502
+ output.address = template.address;
503
+
504
+ if (template.isCurrencySelected) {
505
+ output.setCryptoAmount(template.amount);
506
+ } else {
507
+ sendViewModel.setFiatCurrency(fiatFromTemplate);
508
+ output.setFiatAmount(template.amountFiat);
509
+ }
510
+
511
+ output.resetParsedAddress();
512
+ await output.fetchParsedAddress(context);
513
+ }
514
+
515
Output _defineCurrentOutput() {
516
if (controller.page == null) {
517
throw Exception('Controller page is null');
lib/src/screens/send/send_template_page.dart
+139
-280
@@ -1,35 +1,21 @@
1
-import 'package:cake_wallet/utils/payment_request.dart';
1
+import 'package:cake_wallet/src/widgets/trail_button.dart';
2
+import 'package:cake_wallet/view_model/send/template_view_model.dart';
3
import 'package:flutter_mobx/flutter_mobx.dart';
3
-import 'package:mobx/mobx.dart';
4
-import 'package:flutter/cupertino.dart';
4
import 'package:flutter/material.dart';
6
-import 'package:flutter/services.dart';
7
-import 'package:keyboard_actions/keyboard_actions.dart';
5
import 'package:cake_wallet/src/screens/base_page.dart';
6
import 'package:cake_wallet/generated/i18n.dart';
7
import 'package:cake_wallet/view_model/send/send_template_view_model.dart';
11
-import 'package:cake_wallet/src/widgets/address_text_field.dart';
12
-import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
13
-import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
8
import 'package:cake_wallet/src/widgets/primary_button.dart';
9
import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart';
16
-import 'package:cake_wallet/src/screens/send/widgets/prefix_currency_icon_widget.dart';
10
+import 'package:cake_wallet/src/screens/send/widgets/send_template_card.dart';
11
+import 'package:smooth_page_indicator/smooth_page_indicator.dart';
12
13
class SendTemplatePage extends BasePage {
19
- SendTemplatePage({required this.sendTemplateViewModel}) {
20
- sendTemplateViewModel.output.reset();
21
- }
14
+ SendTemplatePage({required this.sendTemplateViewModel});
15
16
final SendTemplateViewModel sendTemplateViewModel;
24
- final _addressController = TextEditingController();
25
- final _cryptoAmountController = TextEditingController();
26
- final _fiatAmountController = TextEditingController();
27
- final _nameController = TextEditingController();
17
final _formKey = GlobalKey<FormState>();
29
- final FocusNode _cryptoAmountFocus = FocusNode();
30
- final FocusNode _fiatAmountFocus = FocusNode();
31
-
32
- bool _effectsInstalled = false;
18
+ final controller = PageController(initialPage: 0);
19
20
@override
21
String get title => S.current.exchange_new_template;
@@ -44,273 +30,146 @@ class SendTemplatePage extends BasePage {
30
AppBarStyle get appBarStyle => AppBarStyle.transparent;
31
32
@override
47
- Widget body(BuildContext context) {
48
- _setEffects(context);
33
+ Widget trailing(context) => Observer(builder: (_) {
34
+ return sendTemplateViewModel.recipients.length > 1
35
+ ? TrailButton(
36
+ caption: S.of(context).remove,
37
+ onPressed: () {
38
+ int pageToJump = (controller.page?.round() ?? 0) - 1;
39
+ pageToJump = pageToJump > 0 ? pageToJump : 0;
40
+ final recipient = _defineCurrentRecipient();
41
+ sendTemplateViewModel.removeRecipient(recipient);
42
+ controller.jumpToPage(pageToJump);
43
+ })
44
+ : TrailButton(
45
+ caption: S.of(context).clear,
46
+ onPressed: () {
47
+ final recipient = _defineCurrentRecipient();
48
+ _formKey.currentState?.reset();
49
+ recipient.reset();
50
+ });
51
+ });
52
50
- return KeyboardActions(
51
- config: KeyboardActionsConfig(
52
- keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
53
- keyboardBarColor: Theme.of(context)
54
- .accentTextTheme!
55
- .bodyLarge!
56
- .backgroundColor!,
57
- nextFocus: false,
58
- actions: [
59
- KeyboardActionsItem(
60
- focusNode: _cryptoAmountFocus,
61
- toolbarButtons: [(_) => KeyboardDoneButton()],
62
- ),
63
- KeyboardActionsItem(
64
- focusNode: _fiatAmountFocus,
65
- toolbarButtons: [(_) => KeyboardDoneButton()],
66
- )
67
- ]),
68
- child: Container(
69
- height: 0,
70
- color: Theme.of(context).colorScheme.background,
71
- child: ScrollableWithBottomSection(
53
+ @override
54
+ Widget body(BuildContext context) {
55
+ return Form(
56
+ key: _formKey,
57
+ child: ScrollableWithBottomSection(
58
contentPadding: EdgeInsets.only(bottom: 24),
73
- content: Container(
74
- decoration: BoxDecoration(
75
- borderRadius: BorderRadius.only(
76
- bottomLeft: Radius.circular(24),
77
- bottomRight: Radius.circular(24),
78
- ),
79
- gradient: LinearGradient(colors: [
80
- Theme.of(context).primaryTextTheme!.titleMedium!.color!,
81
- Theme.of(context)
82
- .primaryTextTheme!
83
- .titleMedium!
84
- .decorationColor!,
85
- ], begin: Alignment.topLeft, end: Alignment.bottomRight),
86
- ),
87
- child: Form(
88
- key: _formKey,
89
- child: Column(
90
- children: <Widget>[
91
- Padding(
92
- padding: EdgeInsets.fromLTRB(24, 90, 24, 32),
93
- child: Column(
94
- children: <Widget>[
95
- BaseTextFormField(
96
- controller: _nameController,
97
- hintText: S.of(context).send_name,
98
- borderColor: Theme.of(context)
99
- .primaryTextTheme!
100
- .headlineSmall!
101
- .color!,
102
- textStyle: TextStyle(
103
- fontSize: 14,
104
- fontWeight: FontWeight.w500,
105
- color: Colors.white),
106
- placeholderTextStyle: TextStyle(
107
- color: Theme.of(context)
108
- .primaryTextTheme!
109
- .headlineSmall!
110
- .decorationColor!,
111
- fontWeight: FontWeight.w500,
112
- fontSize: 14),
113
- validator: sendTemplateViewModel.templateValidator,
114
- ),
115
- Padding(
116
- padding: EdgeInsets.only(top: 20),
117
- child: AddressTextField(
118
- controller: _addressController,
119
- onURIScanned: (uri) {
120
- final paymentRequest = PaymentRequest.fromUri(uri);
121
- _addressController.text = paymentRequest.address;
122
- _cryptoAmountController.text = paymentRequest.amount;
123
- },
124
- options: [
125
- AddressTextFieldOption.paste,
126
- AddressTextFieldOption.qrCode,
127
- AddressTextFieldOption.addressBook
128
- ],
129
- buttonColor: Theme.of(context)
130
- .primaryTextTheme!
131
- .headlineMedium!
132
- .color!,
133
- borderColor: Theme.of(context)
134
- .primaryTextTheme!
135
- .headlineSmall!
136
- .color!,
137
- textStyle: TextStyle(
138
- fontSize: 14,
139
- fontWeight: FontWeight.w500,
140
- color: Colors.white),
141
- hintStyle: TextStyle(
142
- fontSize: 14,
143
- fontWeight: FontWeight.w500,
144
- color: Theme.of(context)
145
- .primaryTextTheme!
146
- .headlineSmall!
147
- .decorationColor!),
148
- ),
149
- ),
150
- Padding(
151
- padding: const EdgeInsets.only(top: 20),
152
- child: Focus(
153
- onFocusChange: (hasFocus) {
154
- if (hasFocus) {
155
- sendTemplateViewModel.selectCurrency();
156
- }
157
- },
158
- child: BaseTextFormField(
159
- focusNode: _cryptoAmountFocus,
160
- controller: _cryptoAmountController,
161
- keyboardType: TextInputType.numberWithOptions(
162
- signed: false, decimal: true),
163
- inputFormatters: [
164
- FilteringTextInputFormatter.deny(
165
- RegExp('[\\-|\\ ]'))
166
- ],
167
- prefixIcon: Observer(
168
- builder: (_) => PrefixCurrencyIcon(
169
- title: sendTemplateViewModel
170
- .currency.title,
171
- isSelected:
172
- sendTemplateViewModel
173
- .isCurrencySelected,
174
- )),
175
- hintText: '0.0000',
176
- borderColor: Theme.of(context)
177
- .primaryTextTheme!
178
- .headlineSmall!
179
- .color!,
180
- textStyle: TextStyle(
181
- fontSize: 14,
182
- fontWeight: FontWeight.w500,
183
- color: Colors.white),
184
- placeholderTextStyle: TextStyle(
185
- color: Theme.of(context)
186
- .primaryTextTheme!
187
- .headlineSmall!
188
- .decorationColor!,
189
- fontWeight: FontWeight.w500,
190
- fontSize: 14),
191
- validator:
192
- sendTemplateViewModel.amountValidator))),
193
- Padding(
194
- padding: const EdgeInsets.only(top: 20),
195
- child: Focus(
196
- onFocusChange: (hasFocus) {
197
- if (hasFocus) {
198
- sendTemplateViewModel.selectFiat();
199
- }
200
- },
201
- child: BaseTextFormField(
202
- focusNode: _fiatAmountFocus,
203
- controller: _fiatAmountController,
204
- keyboardType: TextInputType.numberWithOptions(
205
- signed: false, decimal: true),
206
- inputFormatters: [
207
- FilteringTextInputFormatter.deny(
208
- RegExp('[\\-|\\ ]'))
209
- ],
210
- prefixIcon: Observer(
211
- builder: (_) => PrefixCurrencyIcon(
212
- title: sendTemplateViewModel
213
- .fiat.title,
214
- isSelected: sendTemplateViewModel
215
- .isFiatSelected,
216
- )),
217
- hintText: '0.00',
218
- borderColor: Theme.of(context)
219
- .primaryTextTheme!
220
- .headlineSmall!
221
- .color!,
222
- textStyle: TextStyle(
223
- fontSize: 14,
224
- fontWeight: FontWeight.w500,
225
- color: Colors.white),
226
- placeholderTextStyle: TextStyle(
227
- color: Theme.of(context)
228
- .primaryTextTheme!
229
- .headlineSmall!
230
- .decorationColor!,
231
- fontWeight: FontWeight.w500,
232
- fontSize: 14),
233
- ))),
234
- ],
59
+ content: FocusTraversalGroup(
60
+ policy: OrderedTraversalPolicy(),
61
+ child: Column(children: [
62
+ Container(
63
+ height: 460,
64
+ child: Observer(builder: (_) {
65
+ return PageView.builder(
66
+ scrollDirection: Axis.horizontal,
67
+ controller: controller,
68
+ itemCount: sendTemplateViewModel.recipients.length,
69
+ itemBuilder: (_, index) {
70
+ final template =
71
+ sendTemplateViewModel.recipients[index];
72
+ return SendTemplateCard(
73
+ template: template,
74
+ index: index,
75
+ sendTemplateViewModel: sendTemplateViewModel);
76
+ });
77
+ })),
78
+ Padding(
79
+ padding: EdgeInsets.only(
80
+ top: 10, left: 24, right: 24, bottom: 10),
81
+ child: Container(
82
+ height: 10,
83
+ child: Observer(
84
+ builder: (_) {
85
+ final count = sendTemplateViewModel.recipients.length;
86
+
87
+ return count > 1
88
+ ? SmoothPageIndicator(
89
+ controller: controller,
90
+ count: count,
91
+ effect: ScrollingDotsEffect(
92
+ spacing: 6.0,
93
+ radius: 6.0,
94
+ dotWidth: 6.0,
95
+ dotHeight: 6.0,
96
+ dotColor: Theme.of(context)
97
+ .primaryTextTheme
98
+ .displaySmall!
99
+ .backgroundColor!,
100
+ activeDotColor: Theme.of(context)
101
+ .primaryTextTheme
102
+ .displayMedium!
103
+ .backgroundColor!))
104
+ : Offstage();
105
+ },
106
),
236
- )
237
- ],
238
- ),
239
- ),
240
- ),
107
+ ),
108
+ ),
109
+ ])),
110
bottomSectionPadding:
111
EdgeInsets.only(left: 24, right: 24, bottom: 24),
243
- bottomSection: PrimaryButton(
244
- onPressed: () {
245
- if (_formKey.currentState != null && _formKey.currentState!.validate()) {
246
- sendTemplateViewModel.addTemplate(
247
- isCurrencySelected: sendTemplateViewModel.isCurrencySelected,
248
- name: _nameController.text,
249
- address: _addressController.text,
250
- cryptoCurrency:sendTemplateViewModel.currency.title,
251
- fiatCurrency: sendTemplateViewModel.fiat.title,
252
- amount: _cryptoAmountController.text,
253
- amountFiat: _fiatAmountController.text);
254
- Navigator.of(context).pop();
255
- }
256
- },
257
- text: S.of(context).save,
258
- color: Colors.green,
259
- textColor: Colors.white,
260
- ),
261
- ),
262
- ));
112
+ bottomSection: Column(children: [
113
+ // if (sendViewModel.hasMultiRecipient)
114
+ Padding(
115
+ padding: EdgeInsets.only(bottom: 12),
116
+ child: PrimaryButton(
117
+ onPressed: () {
118
+ sendTemplateViewModel.addRecipient();
119
+ Future.delayed(const Duration(milliseconds: 250), () {
120
+ controller.jumpToPage(
121
+ sendTemplateViewModel.recipients.length - 1);
122
+ });
123
+ },
124
+ text: S.of(context).add_receiver,
125
+ color: Colors.transparent,
126
+ textColor: Theme.of(context)
127
+ .accentTextTheme
128
+ .displaySmall!
129
+ .decorationColor!,
130
+ isDottedBorder: true,
131
+ borderColor: Theme.of(context)
132
+ .primaryTextTheme
133
+ .displaySmall!
134
+ .decorationColor!)),
135
+ PrimaryButton(
136
+ onPressed: () {
137
+ if (_formKey.currentState != null &&
138
+ _formKey.currentState!.validate()) {
139
+ final mainTemplate = sendTemplateViewModel.recipients[0];
140
+ print(sendTemplateViewModel.recipients.map((element) =>
141
+ element.toTemplate(
142
+ cryptoCurrency:
143
+ sendTemplateViewModel.cryptoCurrency.title,
144
+ fiatCurrency:
145
+ sendTemplateViewModel.fiatCurrency)));
146
+ sendTemplateViewModel.addTemplate(
147
+ isCurrencySelected: mainTemplate.isCurrencySelected,
148
+ name: mainTemplate.name,
149
+ address: mainTemplate.address,
150
+ amount: mainTemplate.output.cryptoAmount,
151
+ amountFiat: mainTemplate.output.fiatAmount,
152
+ additionalRecipients: sendTemplateViewModel.recipients
153
+ .map((element) => element.toTemplate(
154
+ cryptoCurrency: sendTemplateViewModel
155
+ .cryptoCurrency.title,
156
+ fiatCurrency:
157
+ sendTemplateViewModel.fiatCurrency))
158
+ .toList());
159
+ Navigator.of(context).pop();
160
+ }
161
+ },
162
+ text: S.of(context).save,
163
+ color: Colors.green,
164
+ textColor: Colors.white)
165
+ ])));
166
}
167
265
- void _setEffects(BuildContext context) {
266
- if (_effectsInstalled) {
267
- return;
168
+ TemplateViewModel _defineCurrentRecipient() {
169
+ if (controller.page == null) {
170
+ throw Exception('Controller page is null');
171
}
269
-
270
- final output = sendTemplateViewModel.output;
271
-
272
- reaction((_) => output.fiatAmount, (String amount) {
273
- if (amount != _fiatAmountController.text) {
274
- _fiatAmountController.text = amount;
275
- }
276
- });
277
-
278
- reaction((_) => output.cryptoAmount, (String amount) {
279
- if (amount != _cryptoAmountController.text) {
280
- _cryptoAmountController.text = amount;
281
- }
282
- });
283
-
284
- reaction((_) => output.address, (String address) {
285
- if (address != _addressController.text) {
286
- _addressController.text = address;
287
- }
288
- });
289
-
290
- _cryptoAmountController.addListener(() {
291
- final amount = _cryptoAmountController.text;
292
-
293
- if (amount != output.cryptoAmount) {
294
- output.setCryptoAmount(amount);
295
- }
296
- });
297
-
298
- _fiatAmountController.addListener(() {
299
- final amount = _fiatAmountController.text;
300
-
301
- if (amount != output.fiatAmount) {
302
- output.setFiatAmount(amount);
303
- }
304
- });
305
-
306
- _addressController.addListener(() {
307
- final address = _addressController.text;
308
-
309
- if (output.address != address) {
310
- output.address = address;
311
- }
312
- });
313
-
314
- _effectsInstalled = true;
172
+ final itemCount = controller.page!.round();
173
+ return sendTemplateViewModel.recipients[itemCount];
174
}
175
}
lib/src/screens/send/widgets/send_template_card.dart
new
+267
@@ -0,0 +1,267 @@
1
+import 'package:cake_wallet/src/screens/send/widgets/prefix_currency_icon_widget.dart';
2
+import 'package:cake_wallet/utils/payment_request.dart';
3
+import 'package:cake_wallet/view_model/send/template_view_model.dart';
4
+import 'package:flutter_mobx/flutter_mobx.dart';
5
+import 'package:flutter/material.dart';
6
+import 'package:flutter/services.dart';
7
+import 'package:cake_wallet/generated/i18n.dart';
8
+import 'package:cake_wallet/view_model/send/send_template_view_model.dart';
9
+import 'package:cake_wallet/src/widgets/address_text_field.dart';
10
+import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
11
+import 'package:mobx/mobx.dart';
12
+
13
+class SendTemplateCard extends StatelessWidget {
14
+ SendTemplateCard(
15
+ {super.key,
16
+ required this.template,
17
+ required this.index,
18
+ required this.sendTemplateViewModel});
19
+
20
+ final TemplateViewModel template;
21
+ final int index;
22
+ final SendTemplateViewModel sendTemplateViewModel;
23
+
24
+ final _addressController = TextEditingController();
25
+ final _cryptoAmountController = TextEditingController();
26
+ final _fiatAmountController = TextEditingController();
27
+ final _nameController = TextEditingController();
28
+ final FocusNode _cryptoAmountFocus = FocusNode();
29
+ final FocusNode _fiatAmountFocus = FocusNode();
30
+
31
+ bool _effectsInstalled = false;
32
+
33
+ @override
34
+ Widget build(BuildContext context) {
35
+ _setEffects(context);
36
+
37
+ return Container(
38
+ decoration: BoxDecoration(
39
+ borderRadius: BorderRadius.only(
40
+ bottomLeft: Radius.circular(24),
41
+ bottomRight: Radius.circular(24)),
42
+ gradient: LinearGradient(colors: [
43
+ Theme.of(context).primaryTextTheme.titleMedium!.color!,
44
+ Theme.of(context).primaryTextTheme.titleMedium!.decorationColor!
45
+ ], begin: Alignment.topLeft, end: Alignment.bottomRight)),
46
+ child: Column(children: <Widget>[
47
+ Padding(
48
+ padding: EdgeInsets.fromLTRB(24, 90, 24, 32),
49
+ child: Column(children: <Widget>[
50
+ if (index == 0)
51
+ BaseTextFormField(
52
+ controller: _nameController,
53
+ hintText: sendTemplateViewModel.recipients.length > 1
54
+ ? S.of(context).template_name
55
+ : S.of(context).send_name,
56
+ borderColor: Theme.of(context)
57
+ .primaryTextTheme
58
+ .headlineSmall!
59
+ .color!,
60
+ textStyle: TextStyle(
61
+ fontSize: 14,
62
+ fontWeight: FontWeight.w500,
63
+ color: Colors.white),
64
+ placeholderTextStyle: TextStyle(
65
+ color: Theme.of(context)
66
+ .primaryTextTheme
67
+ .headlineSmall!
68
+ .decorationColor!,
69
+ fontWeight: FontWeight.w500,
70
+ fontSize: 14),
71
+ validator: sendTemplateViewModel.templateValidator),
72
+ Padding(
73
+ padding: EdgeInsets.only(top: 20),
74
+ child: AddressTextField(
75
+ selectedCurrency: sendTemplateViewModel.cryptoCurrency,
76
+ controller: _addressController,
77
+ onURIScanned: (uri) {
78
+ final paymentRequest = PaymentRequest.fromUri(uri);
79
+ _addressController.text = paymentRequest.address;
80
+ _cryptoAmountController.text = paymentRequest.amount;
81
+ },
82
+ options: [
83
+ AddressTextFieldOption.paste,
84
+ AddressTextFieldOption.qrCode,
85
+ AddressTextFieldOption.addressBook
86
+ ],
87
+ onPushPasteButton: (context) async {
88
+ template.output.resetParsedAddress();
89
+ await template.output.fetchParsedAddress(context);
90
+ },
91
+ onPushAddressBookButton: (context) async {
92
+ template.output.resetParsedAddress();
93
+ await template.output.fetchParsedAddress(context);
94
+ },
95
+ buttonColor: Theme.of(context)
96
+ .primaryTextTheme
97
+ .headlineMedium!
98
+ .color!,
99
+ borderColor: Theme.of(context)
100
+ .primaryTextTheme
101
+ .headlineSmall!
102
+ .color!,
103
+ textStyle: TextStyle(
104
+ fontSize: 14,
105
+ fontWeight: FontWeight.w500,
106
+ color: Colors.white),
107
+ hintStyle: TextStyle(
108
+ fontSize: 14,
109
+ fontWeight: FontWeight.w500,
110
+ color: Theme.of(context)
111
+ .primaryTextTheme
112
+ .headlineSmall!
113
+ .decorationColor!),
114
+ validator: sendTemplateViewModel.addressValidator)),
115
+ Padding(
116
+ padding: const EdgeInsets.only(top: 20),
117
+ child: Focus(
118
+ onFocusChange: (hasFocus) {
119
+ if (hasFocus) {
120
+ template.selectCurrency();
121
+ }
122
+ },
123
+ child: BaseTextFormField(
124
+ focusNode: _cryptoAmountFocus,
125
+ controller: _cryptoAmountController,
126
+ keyboardType: TextInputType.numberWithOptions(
127
+ signed: false, decimal: true),
128
+ inputFormatters: [
129
+ FilteringTextInputFormatter.deny(
130
+ RegExp('[\\-|\\ ]'))
131
+ ],
132
+ prefixIcon: Observer(
133
+ builder: (_) => PrefixCurrencyIcon(
134
+ title: sendTemplateViewModel
135
+ .cryptoCurrency.title,
136
+ isSelected: template.isCurrencySelected)),
137
+ hintText: '0.0000',
138
+ borderColor: Theme.of(context)
139
+ .primaryTextTheme
140
+ .headlineSmall!
141
+ .color!,
142
+ textStyle: TextStyle(
143
+ fontSize: 14,
144
+ fontWeight: FontWeight.w500,
145
+ color: Colors.white),
146
+ placeholderTextStyle: TextStyle(
147
+ color: Theme.of(context)
148
+ .primaryTextTheme
149
+ .headlineSmall!
150
+ .decorationColor!,
151
+ fontWeight: FontWeight.w500,
152
+ fontSize: 14),
153
+ validator: sendTemplateViewModel.amountValidator))),
154
+ Padding(
155
+ padding: const EdgeInsets.only(top: 20),
156
+ child: Focus(
157
+ onFocusChange: (hasFocus) {
158
+ if (hasFocus) {
159
+ template.selectFiat();
160
+ }
161
+ },
162
+ child: BaseTextFormField(
163
+ focusNode: _fiatAmountFocus,
164
+ controller: _fiatAmountController,
165
+ keyboardType: TextInputType.numberWithOptions(
166
+ signed: false, decimal: true),
167
+ inputFormatters: [
168
+ FilteringTextInputFormatter.deny(
169
+ RegExp('[\\-|\\ ]'))
170
+ ],
171
+ prefixIcon: Observer(
172
+ builder: (_) => PrefixCurrencyIcon(
173
+ title: sendTemplateViewModel.fiatCurrency,
174
+ isSelected: template.isFiatSelected)),
175
+ hintText: '0.00',
176
+ borderColor: Theme.of(context)
177
+ .primaryTextTheme
178
+ .headlineSmall!
179
+ .color!,
180
+ textStyle: TextStyle(
181
+ fontSize: 14,
182
+ fontWeight: FontWeight.w500,
183
+ color: Colors.white),
184
+ placeholderTextStyle: TextStyle(
185
+ color: Theme.of(context)
186
+ .primaryTextTheme
187
+ .headlineSmall!
188
+ .decorationColor!,
189
+ fontWeight: FontWeight.w500,
190
+ fontSize: 14))))
191
+ ]))
192
+ ]));
193
+ }
194
+
195
+ void _setEffects(BuildContext context) {
196
+ if (_effectsInstalled) {
197
+ return;
198
+ }
199
+
200
+ final output = template.output;
201
+
202
+ if (template.address.isNotEmpty) {
203
+ _addressController.text = template.address;
204
+ }
205
+ if (template.name.isNotEmpty) {
206
+ _nameController.text = template.name;
207
+ }
208
+ if (template.output.cryptoAmount.isNotEmpty) {
209
+ _cryptoAmountController.text = template.output.cryptoAmount;
210
+ }
211
+ if (template.output.fiatAmount.isNotEmpty) {
212
+ _fiatAmountController.text = template.output.fiatAmount;
213
+ }
214
+
215
+ _addressController.addListener(() {
216
+ final address = _addressController.text;
217
+
218
+ if (template.address != address) {
219
+ template.address = address;
220
+ }
221
+ });
222
+ _cryptoAmountController.addListener(() {
223
+ final amount = _cryptoAmountController.text;
224
+
225
+ if (amount != output.cryptoAmount) {
226
+ output.setCryptoAmount(amount);
227
+ }
228
+ });
229
+ _fiatAmountController.addListener(() {
230
+ final amount = _fiatAmountController.text;
231
+
232
+ if (amount != output.fiatAmount) {
233
+ output.setFiatAmount(amount);
234
+ }
235
+ });
236
+ _nameController.addListener(() {
237
+ final name = _nameController.text;
238
+
239
+ if (name != template.name) {
240
+ template.name = name;
241
+ }
242
+ });
243
+
244
+ reaction((_) => template.address, (String address) {
245
+ if (address != _addressController.text) {
246
+ _addressController.text = address;
247
+ }
248
+ });
249
+ reaction((_) => output.cryptoAmount, (String amount) {
250
+ if (amount != _cryptoAmountController.text) {
251
+ _cryptoAmountController.text = amount;
252
+ }
253
+ });
254
+ reaction((_) => output.fiatAmount, (String amount) {
255
+ if (amount != _fiatAmountController.text) {
256
+ _fiatAmountController.text = amount;
257
+ }
258
+ });
259
+ reaction((_) => template.name, (String name) {
260
+ if (name != _nameController.text) {
261
+ _nameController.text = name;
262
+ }
263
+ });
264
+
265
+ _effectsInstalled = true;
266
+ }
267
+}
lib/src/widgets/template_tile.dart
+44
-40
@@ -8,7 +8,8 @@ class TemplateTile extends StatefulWidget {
8
required this.amount,
9
required this.from,
10
required this.onTap,
11
- required this.onRemove
11
+ required this.onRemove,
12
+ this.hasMultipleRecipients,
13
}) : super(key: key);
14
15
final String to;
@@ -16,6 +17,7 @@ class TemplateTile extends StatefulWidget {
17
final String from;
18
final VoidCallback onTap;
19
final VoidCallback onRemove;
20
+ final bool? hasMultipleRecipients;
21
22
@override
23
TemplateTileState createState() => TemplateTileState(
@@ -51,45 +53,47 @@ class TemplateTileState extends State<TemplateTile> {
53
final toIcon = Image.asset('assets/images/to_icon.png', color: color);
54
55
final content = Row(
54
- mainAxisAlignment: MainAxisAlignment.start,
55
- mainAxisSize: MainAxisSize.min,
56
- children: <Widget>[
57
- Text(
58
- amount,
59
- style: TextStyle(
60
- fontSize: 16,
61
- fontWeight: FontWeight.w600,
62
- color: color
63
- ),
64
- ),
65
- Padding(
66
- padding: EdgeInsets.only(left: 5),
67
- child: Text(
68
- from,
69
- style: TextStyle(
70
- fontSize: 16,
71
- fontWeight: FontWeight.w600,
72
- color: color
73
- ),
74
- ),
75
- ),
76
- Padding(
77
- padding: EdgeInsets.only(left: 5),
78
- child: toIcon,
79
- ),
80
- Padding(
81
- padding: EdgeInsets.only(left: 5),
82
- child: Text(
83
- to,
84
- style: TextStyle(
85
- fontSize: 16,
86
- fontWeight: FontWeight.w600,
87
- color: color
88
- ),
89
- ),
90
- ),
91
- ],
92
- );
56
+ mainAxisAlignment: MainAxisAlignment.start,
57
+ mainAxisSize: MainAxisSize.min,
58
+ children: widget.hasMultipleRecipients ?? false
59
+ ? [
60
+ Text(
61
+ to,
62
+ style: TextStyle(
63
+ fontSize: 16, fontWeight: FontWeight.w600, color: color),
64
+ ),
65
+ ]
66
+ : [
67
+ Text(
68
+ amount,
69
+ style: TextStyle(
70
+ fontSize: 16, fontWeight: FontWeight.w600, color: color),
71
+ ),
72
+ Padding(
73
+ padding: EdgeInsets.only(left: 5),
74
+ child: Text(
75
+ from,
76
+ style: TextStyle(
77
+ fontSize: 16,
78
+ fontWeight: FontWeight.w600,
79
+ color: color),
80
+ ),
81
+ ),
82
+ Padding(
83
+ padding: EdgeInsets.only(left: 5),
84
+ child: toIcon,
85
+ ),
86
+ Padding(
87
+ padding: EdgeInsets.only(left: 5),
88
+ child: Text(
89
+ to,
90
+ style: TextStyle(
91
+ fontSize: 16,
92
+ fontWeight: FontWeight.w600,
93
+ color: color),
94
+ ),
95
+ ),
96
+ ]);
97
98
final tile = Container(
99
padding: EdgeInsets.only(right: 10),
lib/store/templates/send_template_store.dart
+18
-16
@@ -23,25 +23,27 @@ abstract class SendTemplateBase with Store {
23
templates.replaceRange(0, templates.length, templateSource.values.toList());
24
25
@action
26
- Future<void> addTemplate({
27
- required String name,
28
- required bool isCurrencySelected,
29
- required String address,
30
- required String cryptoCurrency,
31
- required String fiatCurrency,
32
- required String amount,
33
- required String amountFiat}) async {
26
+ Future<void> addTemplate(
27
+ {required String name,
28
+ required bool isCurrencySelected,
29
+ required String address,
30
+ required String cryptoCurrency,
31
+ required String fiatCurrency,
32
+ required String amount,
33
+ required String amountFiat,
34
+ required List<Template> additionalRecipients}) async {
35
final template = Template(
35
- nameRaw: name,
36
- isCurrencySelectedRaw: isCurrencySelected,
37
- addressRaw: address,
38
- cryptoCurrencyRaw: cryptoCurrency,
39
- fiatCurrencyRaw: fiatCurrency,
40
- amountRaw: amount,
41
- amountFiatRaw: amountFiat);
36
+ nameRaw: name,
37
+ isCurrencySelectedRaw: isCurrencySelected,
38
+ addressRaw: address,
39
+ cryptoCurrencyRaw: cryptoCurrency,
40
+ fiatCurrencyRaw: fiatCurrency,
41
+ amountRaw: amount,
42
+ amountFiatRaw: amountFiat,
43
+ additionalRecipientsRaw: additionalRecipients);
44
await templateSource.add(template);
45
}
46
47
@action
48
Future<void> remove({required Template template}) async => await template.delete();
47
-}
\ No newline at end of file
49
+}
lib/view_model/send/send_template_view_model.dart
+42
-42
@@ -1,4 +1,5 @@
1
-import 'package:cake_wallet/view_model/send/output.dart';
1
+import 'package:cake_wallet/view_model/send/template_view_model.dart';
2
+import 'package:cw_core/crypto_currency.dart';
3
import 'package:cw_core/wallet_type.dart';
4
import 'package:mobx/mobx.dart';
5
import 'package:cake_wallet/entities/template.dart';
@@ -6,10 +7,7 @@ import 'package:cake_wallet/store/templates/send_template_store.dart';
7
import 'package:cake_wallet/core/template_validator.dart';
8
import 'package:cake_wallet/core/address_validator.dart';
9
import 'package:cake_wallet/core/amount_validator.dart';
9
-import 'package:cake_wallet/core/validator.dart';
10
import 'package:cw_core/wallet_base.dart';
11
-import 'package:cw_core/crypto_currency.dart';
12
-import 'package:cake_wallet/entities/fiat_currency.dart';
11
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
12
import 'package:cake_wallet/store/settings_store.dart';
13
@@ -19,72 +17,74 @@ class SendTemplateViewModel = SendTemplateViewModelBase
17
with _$SendTemplateViewModel;
18
19
abstract class SendTemplateViewModelBase with Store {
20
+ final WalletBase _wallet;
21
+ final SettingsStore _settingsStore;
22
+ final SendTemplateStore _sendTemplateStore;
23
+ final FiatConversionStore _fiatConversationStore;
24
+
25
SendTemplateViewModelBase(this._wallet, this._settingsStore,
26
this._sendTemplateStore, this._fiatConversationStore)
24
- : output = Output(_wallet, _settingsStore, _fiatConversationStore, () => _wallet.currency) {
25
- output = Output(_wallet, _settingsStore, _fiatConversationStore, () => currency);
27
+ : recipients = ObservableList<TemplateViewModel>() {
28
+ addRecipient();
29
}
30
28
- Output output;
29
-
30
- Validator get amountValidator =>
31
- AmountValidator(currency: walletTypeToCryptoCurrency(_wallet.type));
32
-
33
- Validator get addressValidator => AddressValidator(type: _wallet.currency);
31
+ ObservableList<TemplateViewModel> recipients;
32
35
- Validator get templateValidator => TemplateValidator();
33
+ @action
34
+ void addRecipient() {
35
+ recipients.add(TemplateViewModel(
36
+ cryptoCurrency: cryptoCurrency,
37
+ wallet: _wallet,
38
+ settingsStore: _settingsStore,
39
+ fiatConversationStore: _fiatConversationStore));
40
+ }
41
37
- CryptoCurrency get currency => _wallet.currency;
42
+ @action
43
+ void removeRecipient(TemplateViewModel recipient) {
44
+ recipients.remove(recipient);
45
+ }
46
39
- FiatCurrency get fiat => _settingsStore.fiatCurrency;
47
+ AmountValidator get amountValidator =>
48
+ AmountValidator(currency: walletTypeToCryptoCurrency(_wallet.type));
49
41
- @observable
42
- bool isCurrencySelected = true;
50
+ AddressValidator get addressValidator =>
51
+ AddressValidator(type: _wallet.currency);
52
44
- @observable
45
- bool isFiatSelected = false;
53
+ TemplateValidator get templateValidator => TemplateValidator();
54
47
- @action
48
- void selectCurrency () {
49
- isCurrencySelected = true;
50
- isFiatSelected = false;
51
- }
55
+ @computed
56
+ CryptoCurrency get cryptoCurrency => _wallet.currency;
57
53
- @action
54
- void selectFiat () {
55
- isFiatSelected = true;
56
- isCurrencySelected = false;
57
- }
58
+ @computed
59
+ String get fiatCurrency => _settingsStore.fiatCurrency.title;
60
61
@computed
62
ObservableList<Template> get templates => _sendTemplateStore.templates;
63
62
- final WalletBase _wallet;
63
- final SettingsStore _settingsStore;
64
- final SendTemplateStore _sendTemplateStore;
65
- final FiatConversionStore _fiatConversationStore;
66
-
64
+ @action
65
void updateTemplate() => _sendTemplateStore.update();
66
67
+ @action
68
void addTemplate(
69
{required String name,
71
- required bool isCurrencySelected,
72
- required String address,
73
- required String cryptoCurrency,
74
- required String fiatCurrency,
75
- required String amount,
76
- required String amountFiat}) {
70
+ required bool isCurrencySelected,
71
+ required String address,
72
+ required String amount,
73
+ required String amountFiat,
74
+ required List<Template> additionalRecipients}) {
75
_sendTemplateStore.addTemplate(
76
name: name,
77
isCurrencySelected: isCurrencySelected,
78
address: address,
81
- cryptoCurrency: cryptoCurrency,
79
+ cryptoCurrency: cryptoCurrency.title,
80
fiatCurrency: fiatCurrency,
81
amount: amount,
84
- amountFiat: amountFiat);
82
+ amountFiat: amountFiat,
83
+ additionalRecipients: additionalRecipients);
84
updateTemplate();
85
}
86
87
+ @action
88
void removeTemplate({required Template template}) {
89
_sendTemplateStore.remove(template: template);
90
updateTemplate();
lib/view_model/send/template_view_model.dart
new
+80
@@ -0,0 +1,80 @@
1
+import 'package:cake_wallet/entities/template.dart';
2
+import 'package:cake_wallet/view_model/send/output.dart';
3
+import 'package:mobx/mobx.dart';
4
+import 'package:cw_core/wallet_base.dart';
5
+import 'package:cw_core/crypto_currency.dart';
6
+import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
7
+import 'package:cake_wallet/store/settings_store.dart';
8
+
9
+part 'template_view_model.g.dart';
10
+
11
+class TemplateViewModel = TemplateViewModelBase with _$TemplateViewModel;
12
+
13
+abstract class TemplateViewModelBase with Store {
14
+ final CryptoCurrency cryptoCurrency;
15
+ final WalletBase _wallet;
16
+ final SettingsStore _settingsStore;
17
+ final FiatConversionStore _fiatConversationStore;
18
+
19
+ TemplateViewModelBase(
20
+ {required this.cryptoCurrency,
21
+ required WalletBase wallet,
22
+ required SettingsStore settingsStore,
23
+ required FiatConversionStore fiatConversationStore})
24
+ : _wallet = wallet,
25
+ _settingsStore = settingsStore,
26
+ _fiatConversationStore = fiatConversationStore,
27
+ output = Output(wallet, settingsStore, fiatConversationStore,
28
+ () => wallet.currency) {
29
+ output = Output(
30
+ _wallet, _settingsStore, _fiatConversationStore, () => cryptoCurrency);
31
+ }
32
+
33
+ @observable
34
+ Output output;
35
+
36
+ @observable
37
+ String name = '';
38
+
39
+ @observable
40
+ String address = '';
41
+
42
+ @observable
43
+ bool isCurrencySelected = true;
44
+
45
+ @observable
46
+ bool isFiatSelected = false;
47
+
48
+ @action
49
+ void selectCurrency() {
50
+ isCurrencySelected = true;
51
+ isFiatSelected = false;
52
+ }
53
+
54
+ @action
55
+ void selectFiat() {
56
+ isFiatSelected = true;
57
+ isCurrencySelected = false;
58
+ }
59
+
60
+ @action
61
+ void reset() {
62
+ name = '';
63
+ address = '';
64
+ isCurrencySelected = true;
65
+ isFiatSelected = false;
66
+ output.reset();
67
+ }
68
+
69
+ Template toTemplate(
70
+ {required String cryptoCurrency, required String fiatCurrency}) {
71
+ return Template(
72
+ isCurrencySelectedRaw: isCurrencySelected,
73
+ nameRaw: name,
74
+ addressRaw: address,
75
+ cryptoCurrencyRaw: cryptoCurrency,
76
+ fiatCurrencyRaw: fiatCurrency,
77
+ amountRaw: output.cryptoAmount,
78
+ amountFiatRaw: output.fiatAmount);
79
+ }
80
+}
res/values/strings_ar.arb
+2
-1
@@ -635,5 +635,6 @@
635
"generate_name": "توليد الاسم",
636
"balance_page": "صفحة التوازن",
637
"share": "يشارك",
638
- "slidable": "قابل للانزلاق"
638
+ "slidable": "قابل للانزلاق",
639
+ "template_name": "اسم القالب"
640
}
res/values/strings_bg.arb
+2
-1
@@ -631,5 +631,6 @@
631
"generate_name": "Генериране на име",
632
"balance_page": "Страница за баланс",
633
"share": "Дял",
634
- "slidable": "Плъзгащ се"
634
+ "slidable": "Плъзгащ се",
635
+ "template_name": "Име на шаблон"
636
}
res/values/strings_cs.arb
+2
-1
@@ -631,5 +631,6 @@
631
"generate_name": "Generovat jméno",
632
"balance_page": "Stránka zůstatku",
633
"share": "Podíl",
634
- "slidable": "Posuvné"
634
+ "slidable": "Posuvné",
635
+ "template_name": "Název šablony"
636
}
res/values/strings_de.arb
+2
-1
@@ -637,5 +637,6 @@
637
"generate_name": "Namen generieren",
638
"balance_page": "Balance-Seite",
639
"share": "Aktie",
640
- "slidable": "Verschiebbar"
640
+ "slidable": "Verschiebbar",
641
+ "template_name": "Vorlagenname"
642
}
res/values/strings_en.arb
+2
-1
@@ -637,5 +637,6 @@
637
"generate_name": "Generate Name",
638
"balance_page": "Balance Page",
639
"share": "Share",
640
- "slidable": "Slidable"
640
+ "slidable": "Slidable",
641
+ "template_name": "Template Name"
642
}
res/values/strings_es.arb
+2
-1
@@ -637,5 +637,6 @@
637
"generate_name": "Generar nombre",
638
"balance_page": "Página de saldo",
639
"share": "Compartir",
640
- "slidable": "deslizable"
640
+ "slidable": "deslizable",
641
+ "template_name": "Nombre de la plantilla"
642
}
res/values/strings_fr.arb
+2
-1
@@ -637,5 +637,6 @@
637
"generate_name": "Générer un nom",
638
"balance_page": "Page Solde",
639
"share": "Partager",
640
- "slidable": "Glissable"
640
+ "slidable": "Glissable",
641
+ "template_name": "Nom du modèle"
642
}
res/values/strings_ha.arb
+2
-1
@@ -617,6 +617,7 @@
617
"generate_name": "Ƙirƙirar Suna",
618
"balance_page": "Ma'auni Page",
619
"share": "Raba",
620
- "slidable": "Mai iya zamewa"
620
+ "slidable": "Mai iya zamewa",
621
+ "template_name": "Sunan Samfura"
622
}
623
res/values/strings_hi.arb
+2
-1
@@ -637,5 +637,6 @@
637
"generate_name": "नाम जनरेट करें",
638
"balance_page": "बैलेंस पेज",
639
"share": "शेयर करना",
640
- "slidable": "फिसलने लायक"
640
+ "slidable": "फिसलने लायक",
641
+ "template_name": "टेम्पलेट नाम"
642
}
res/values/strings_hr.arb
+2
-1
@@ -637,5 +637,6 @@
637
"generate_name": "Generiraj ime",
638
"balance_page": "Stranica sa stanjem",
639
"share": "Udio",
640
- "slidable": "Klizna"
640
+ "slidable": "Klizna",
641
+ "template_name": "Naziv predloška"
642
}
res/values/strings_id.arb
+2
-1
@@ -627,5 +627,6 @@
627
"generate_name": "Hasilkan Nama",
628
"balance_page": "Halaman Saldo",
629
"share": "Membagikan",
630
- "slidable": "Dapat digeser"
630
+ "slidable": "Dapat digeser",
631
+ "template_name": "Nama Templat"
632
}
res/values/strings_it.arb
+2
-1
@@ -637,5 +637,6 @@
637
"generate_name": "Genera nome",
638
"balance_page": "Pagina di equilibrio",
639
"share": "Condividere",
640
- "slidable": "Scorrevole"
640
+ "slidable": "Scorrevole",
641
+ "template_name": "Nome modello"
642
}
res/values/strings_ja.arb
+2
-1
@@ -637,5 +637,6 @@
637
"generate_name": "名前の生成",
638
"balance_page": "残高ページ",
639
"share": "共有",
640
- "slidable": "スライド可能"
640
+ "slidable": "スライド可能",
641
+ "template_name": "テンプレート名"
642
}
res/values/strings_ko.arb
+2
-1
@@ -637,5 +637,6 @@
637
"generate_name": "이름 생성",
638
"balance_page": "잔액 페이지",
639
"share": "공유하다",
640
- "slidable": "슬라이딩 가능"
640
+ "slidable": "슬라이딩 가능",
641
+ "template_name": "템플릿 이름"
642
}
res/values/strings_my.arb
+2
-1
@@ -637,5 +637,6 @@
637
"generate_name": "အမည်ဖန်တီးပါ။",
638
"balance_page": "လက်ကျန်စာမျက်နှာ",
639
"share": "မျှဝေပါ။",
640
- "slidable": "လျှောချနိုင်သည်။"
640
+ "slidable": "လျှောချနိုင်သည်။",
641
+ "template_name": "နမူနာပုံစံ"
642
}
res/values/strings_nl.arb
+2
-1
@@ -637,5 +637,6 @@
637
"generate_name": "Naam genereren",
638
"balance_page": "Saldo pagina",
639
"share": "Deel",
640
- "slidable": "Verschuifbaar"
640
+ "slidable": "Verschuifbaar",
641
+ "template_name": "Sjabloonnaam"
642
}
res/values/strings_pl.arb
+2
-1
@@ -637,5 +637,6 @@
637
"generate_name": "Wygeneruj nazwę",
638
"balance_page": "Strona salda",
639
"share": "Udział",
640
- "slidable": "Przesuwne"
640
+ "slidable": "Przesuwne",
641
+ "template_name": "Nazwa szablonu"
642
}
res/values/strings_pt.arb
+2
-1
@@ -636,5 +636,6 @@
636
"generate_name": "Gerar nome",
637
"balance_page": "Página de saldo",
638
"share": "Compartilhar",
639
- "slidable": "Deslizável"
639
+ "slidable": "Deslizável",
640
+ "template_name": "Nome do modelo"
641
}
res/values/strings_ru.arb
+2
-1
@@ -637,5 +637,6 @@
637
"generate_name": "Создать имя",
638
"balance_page": "Страница баланса",
639
"share": "Делиться",
640
- "slidable": "Скользящий"
640
+ "slidable": "Скользящий",
641
+ "template_name": "Имя Шаблона"
642
}
res/values/strings_th.arb
+2
-1
@@ -637,5 +637,6 @@
637
"generate_name": "สร้างชื่อ",
638
"balance_page": "หน้ายอดคงเหลือ",
639
"share": "แบ่งปัน",
640
- "slidable": "เลื่อนได้"
640
+ "slidable": "เลื่อนได้",
641
+ "template_name": "ชื่อแม่แบบ"
642
}
res/values/strings_tr.arb
+2
-1
@@ -637,5 +637,6 @@
637
"generate_name": "İsim Oluştur",
638
"balance_page": "Bakiye Sayfası",
639
"share": "Paylaşmak",
640
- "slidable": "kaydırılabilir"
640
+ "slidable": "kaydırılabilir",
641
+ "template_name": "şablon adı"
642
}
res/values/strings_uk.arb
+2
-1
@@ -637,5 +637,6 @@
637
"generate_name": "Згенерувати назву",
638
"balance_page": "Сторінка балансу",
639
"share": "Поділіться",
640
- "slidable": "Розсувний"
640
+ "slidable": "Розсувний",
641
+ "template_name": "Назва шаблону"
642
}
res/values/strings_ur.arb
+2
-1
@@ -631,5 +631,6 @@
631
"generate_name": "نام پیدا کریں۔",
632
"balance_page": "بیلنس صفحہ",
633
"share": "بانٹیں",
634
- "slidable": "سلائیڈ ایبل"
634
+ "slidable": "سلائیڈ ایبل",
635
+ "template_name": "ٹیمپلیٹ کا نام"
636
}
res/values/strings_yo.arb
+2
-1
@@ -633,5 +633,6 @@
633
"generate_name": "Ṣẹda Orukọ",
634
"balance_page": "Oju-iwe iwọntunwọnsi",
635
"share": "Pinpin",
636
- "slidable": "Slidable"
636
+ "slidable": "Slidable",
637
+ "template_name": "Orukọ Awoṣe"
638
}
res/values/strings_zh.arb
+2
-1
@@ -636,5 +636,6 @@
636
"generate_name": "生成名称",
637
"balance_page": "余额页",
638
"share": "分享",
639
- "slidable": "可滑动"
639
+ "slidable": "可滑动",
640
+ "template_name": "模板名称"
641
}