CAKE-345 | fixed add receiver button; applied dotted borders to primary button; applied localization to add receiver button; added scrollbar to confirm_sending_alert.dart
OleksandrSobol committed
Aug 12, 2021 at 17:56 UTC
30a32ab07163d207d13f0b163c10aa4c8cb0ee0e
19 files changed
+314
-182
lib/src/screens/send/send_page.dart
+13
-2
@@ -264,10 +264,21 @@ class SendPage extends BasePage {
264
child: PrimaryButton(
265
onPressed: () {
266
sendViewModel.addOutput();
267
+ Future.delayed(const Duration(milliseconds: 250), () {
268
+ controller.jumpToPage(sendViewModel.outputs.length - 1);
269
+ });
270
},
271
text: S.of(context).add_receiver,
269
- color: Colors.green,
270
- textColor: Colors.white,
272
+ color: Colors.transparent,
273
+ textColor: Theme.of(context)
274
+ .accentTextTheme
275
+ .display2
276
+ .decorationColor,
277
+ isDottedBorder: true,
278
+ borderColor: Theme.of(context)
279
+ .primaryTextTheme
280
+ .display2
281
+ .decorationColor,
282
)
283
),
284
Observer(builder: (_) {
lib/src/screens/send/widgets/confirm_sending_alert.dart
+262
-162
@@ -3,6 +3,7 @@ import 'package:cake_wallet/view_model/send/output.dart';
3
import 'package:flutter/material.dart';
4
import 'package:cake_wallet/src/widgets/base_alert_dialog.dart';
5
import 'package:cake_wallet/generated/i18n.dart';
6
+import 'package:cake_wallet/src/widgets/cake_scrollbar.dart';
7
8
class ConfirmSendingAlert extends BaseAlertDialog {
9
ConfirmSendingAlert({
@@ -18,13 +19,7 @@ class ConfirmSendingAlert extends BaseAlertDialog {
19
@required this.rightButtonText,
20
@required this.actionLeftButton,
21
@required this.actionRightButton,
21
- this.alertBarrierDismissible = true
22
- }) {
23
- itemCount = outputs.length;
24
- recipientTitle = itemCount > 1
25
- ? S.current.transaction_details_recipient_address
26
- : S.current.recipient_address;
27
- }
22
+ this.alertBarrierDismissible = true});
23
24
final String alertTitle;
25
final String amount;
@@ -40,9 +35,6 @@ class ConfirmSendingAlert extends BaseAlertDialog {
35
final VoidCallback actionRightButton;
36
final bool alertBarrierDismissible;
37
43
- String recipientTitle;
44
- int itemCount;
45
-
38
@override
39
String get titleText => alertTitle;
40
@@ -65,131 +57,184 @@ class ConfirmSendingAlert extends BaseAlertDialog {
57
bool get barrierDismissible => alertBarrierDismissible;
58
59
@override
68
- Widget content(BuildContext context) {
69
- return Container(
70
- height: 200,
71
- child: SingleChildScrollView(
72
- child: Column(
73
- children: <Widget>[
74
- Row(
75
- mainAxisSize: MainAxisSize.max,
76
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
77
- crossAxisAlignment: CrossAxisAlignment.start,
78
- children: <Widget>[
79
- Text(
80
- amount,
81
- style: TextStyle(
82
- fontSize: 16,
83
- fontWeight: FontWeight.normal,
84
- fontFamily: 'Lato',
85
- color: Theme.of(context).primaryTextTheme.title.color,
86
- decoration: TextDecoration.none,
87
- ),
88
- ),
89
- Column(
90
- crossAxisAlignment: CrossAxisAlignment.end,
91
- children: [
92
- Text(
93
- amountValue,
94
- style: TextStyle(
95
- fontSize: 18,
96
- fontWeight: FontWeight.w600,
97
- fontFamily: 'Lato',
98
- color: Theme.of(context).primaryTextTheme.title.color,
99
- decoration: TextDecoration.none,
100
- ),
101
- ),
102
- Text(
103
- fiatAmountValue,
104
- style: TextStyle(
105
- fontSize: 12,
106
- fontWeight: FontWeight.w600,
107
- fontFamily: 'Lato',
108
- color: PaletteDark.pigeonBlue,
109
- decoration: TextDecoration.none,
110
- ),
111
- )
112
- ],
113
- )
114
- ],
115
- ),
116
- Padding(
117
- padding: EdgeInsets.only(top: 16),
118
- child: Row(
119
- mainAxisSize: MainAxisSize.max,
120
- mainAxisAlignment: MainAxisAlignment.spaceBetween,
121
- crossAxisAlignment: CrossAxisAlignment.start,
60
+ Widget content(BuildContext context) => ConfirmSendingAlertContent(
61
+ amount: amount,
62
+ amountValue: amountValue,
63
+ fiatAmountValue: fiatAmountValue,
64
+ fee: fee,
65
+ feeValue: feeValue,
66
+ feeFiatAmount: feeFiatAmount,
67
+ outputs: outputs
68
+ );
69
+}
70
+
71
+class ConfirmSendingAlertContent extends StatefulWidget {
72
+ ConfirmSendingAlertContent({
73
+ @required this.amount,
74
+ @required this.amountValue,
75
+ @required this.fiatAmountValue,
76
+ @required this.fee,
77
+ @required this.feeValue,
78
+ @required this.feeFiatAmount,
79
+ @required this.outputs});
80
+
81
+ final String amount;
82
+ final String amountValue;
83
+ final String fiatAmountValue;
84
+ final String fee;
85
+ final String feeValue;
86
+ final String feeFiatAmount;
87
+ final List<Output> outputs;
88
+
89
+ @override
90
+ ConfirmSendingAlertContentState createState() => ConfirmSendingAlertContentState(
91
+ amount: amount,
92
+ amountValue: amountValue,
93
+ fiatAmountValue: fiatAmountValue,
94
+ fee: fee,
95
+ feeValue: feeValue,
96
+ feeFiatAmount: feeFiatAmount,
97
+ outputs: outputs
98
+ );
99
+}
100
+
101
+class ConfirmSendingAlertContentState extends State<ConfirmSendingAlertContent> {
102
+ ConfirmSendingAlertContentState({
103
+ @required this.amount,
104
+ @required this.amountValue,
105
+ @required this.fiatAmountValue,
106
+ @required this.fee,
107
+ @required this.feeValue,
108
+ @required this.feeFiatAmount,
109
+ @required this.outputs}) {
110
+
111
+ itemCount = outputs.length;
112
+ recipientTitle = itemCount > 1
113
+ ? S.current.transaction_details_recipient_address
114
+ : S.current.recipient_address;
115
+ }
116
+
117
+ final String amount;
118
+ final String amountValue;
119
+ final String fiatAmountValue;
120
+ final String fee;
121
+ final String feeValue;
122
+ final String feeFiatAmount;
123
+ final List<Output> outputs;
124
+
125
+ final double backgroundHeight = 160;
126
+ final double thumbHeight = 72;
127
+ ScrollController controller = ScrollController();
128
+ double fromTop = 0;
129
+ String recipientTitle;
130
+ int itemCount;
131
+
132
+ @override
133
+ Widget build(BuildContext context) {
134
+ controller.addListener(() {
135
+ fromTop = controller.hasClients
136
+ ? (controller.offset / controller.position.maxScrollExtent *
137
+ (backgroundHeight - thumbHeight))
138
+ : 0;
139
+ setState(() {});
140
+ });
141
+
142
+ return Stack(
143
+ alignment: Alignment.center,
144
+ clipBehavior: Clip.none,
145
+ children: [
146
+ Container(
147
+ height: 200,
148
+ child: SingleChildScrollView(
149
+ controller: controller,
150
+ child: Column(
151
children: <Widget>[
123
- Text(
124
- fee,
125
- style: TextStyle(
126
- fontSize: 16,
127
- fontWeight: FontWeight.normal,
128
- fontFamily: 'Lato',
129
- color: Theme.of(context).primaryTextTheme.title.color,
130
- decoration: TextDecoration.none,
131
- ),
132
- ),
133
- Column(
134
- crossAxisAlignment: CrossAxisAlignment.end,
135
- children: [
152
+ Row(
153
+ mainAxisSize: MainAxisSize.max,
154
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
155
+ crossAxisAlignment: CrossAxisAlignment.start,
156
+ children: <Widget>[
157
Text(
137
- feeValue,
158
+ amount,
159
style: TextStyle(
139
- fontSize: 18,
140
- fontWeight: FontWeight.w600,
160
+ fontSize: 16,
161
+ fontWeight: FontWeight.normal,
162
fontFamily: 'Lato',
142
- color: Theme.of(context).primaryTextTheme.title.color,
163
+ color: Theme.of(context)
164
+ .primaryTextTheme
165
+ .title
166
+ .color,
167
decoration: TextDecoration.none,
168
),
169
),
146
- Text(
147
- feeFiatAmount,
148
- style: TextStyle(
149
- fontSize: 12,
150
- fontWeight: FontWeight.w600,
151
- fontFamily: 'Lato',
152
- color: PaletteDark.pigeonBlue,
153
- decoration: TextDecoration.none,
154
- ),
170
+ Column(
171
+ crossAxisAlignment: CrossAxisAlignment.end,
172
+ children: [
173
+ Text(
174
+ amountValue,
175
+ style: TextStyle(
176
+ fontSize: 18,
177
+ fontWeight: FontWeight.w600,
178
+ fontFamily: 'Lato',
179
+ color: Theme.of(context)
180
+ .primaryTextTheme
181
+ .title
182
+ .color,
183
+ decoration: TextDecoration.none,
184
+ ),
185
+ ),
186
+ Text(
187
+ fiatAmountValue,
188
+ style: TextStyle(
189
+ fontSize: 12,
190
+ fontWeight: FontWeight.w600,
191
+ fontFamily: 'Lato',
192
+ color: PaletteDark.pigeonBlue,
193
+ decoration: TextDecoration.none,
194
+ ),
195
+ )
196
+ ],
197
)
198
],
157
- )
158
- ],
159
- )
160
- ),
161
- Padding(
162
- padding: EdgeInsets.only(top: 16),
163
- child: Column(
164
- children: [
165
- Text(
166
- '$recipientTitle:',
167
- style: TextStyle(
168
- fontSize: 16,
169
- fontWeight: FontWeight.normal,
170
- fontFamily: 'Lato',
171
- color: Theme.of(context).primaryTextTheme.title.color,
172
- decoration: TextDecoration.none,
199
),
174
- ),
175
- itemCount > 1
176
- ? ListView.builder(
177
- padding: EdgeInsets.only(top: 0),
178
- shrinkWrap: true,
179
- physics: NeverScrollableScrollPhysics(),
180
- itemCount: itemCount,
181
- itemBuilder: (context, index) {
182
- final item = outputs[index];
183
- final _address = item.address;
184
- final _amount =
185
- item.cryptoAmount.replaceAll(',', '.');
186
-
187
- return Column(
188
- children: [
189
- Padding(
190
- padding: EdgeInsets.only(top: 8),
191
- child: Text(
192
- _address,
200
+ Padding(
201
+ padding: EdgeInsets.only(top: 16),
202
+ child: Row(
203
+ mainAxisSize: MainAxisSize.max,
204
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
205
+ crossAxisAlignment: CrossAxisAlignment.start,
206
+ children: <Widget>[
207
+ Text(
208
+ fee,
209
+ style: TextStyle(
210
+ fontSize: 16,
211
+ fontWeight: FontWeight.normal,
212
+ fontFamily: 'Lato',
213
+ color: Theme.of(context)
214
+ .primaryTextTheme
215
+ .title
216
+ .color,
217
+ decoration: TextDecoration.none,
218
+ ),
219
+ ),
220
+ Column(
221
+ crossAxisAlignment: CrossAxisAlignment.end,
222
+ children: [
223
+ Text(
224
+ feeValue,
225
+ style: TextStyle(
226
+ fontSize: 18,
227
+ fontWeight: FontWeight.w600,
228
+ fontFamily: 'Lato',
229
+ color: Theme.of(context)
230
+ .primaryTextTheme
231
+ .title
232
+ .color,
233
+ decoration: TextDecoration.none,
234
+ ),
235
+ ),
236
+ Text(
237
+ feeFiatAmount,
238
style: TextStyle(
239
fontSize: 12,
240
fontWeight: FontWeight.w600,
@@ -198,49 +243,104 @@ class ConfirmSendingAlert extends BaseAlertDialog {
243
decoration: TextDecoration.none,
244
),
245
)
246
+ ],
247
+ )
248
+ ],
249
+ )
250
+ ),
251
+ Padding(
252
+ padding: EdgeInsets.only(top: 16),
253
+ child: Column(
254
+ children: [
255
+ Text(
256
+ '$recipientTitle:',
257
+ style: TextStyle(
258
+ fontSize: 16,
259
+ fontWeight: FontWeight.normal,
260
+ fontFamily: 'Lato',
261
+ color: Theme.of(context)
262
+ .primaryTextTheme
263
+ .title
264
+ .color,
265
+ decoration: TextDecoration.none,
266
),
202
- Padding(
203
- padding: EdgeInsets.only(top: 8),
204
- child: Row(
205
- mainAxisSize: MainAxisSize.max,
206
- mainAxisAlignment: MainAxisAlignment.end,
267
+ ),
268
+ itemCount > 1
269
+ ? ListView.builder(
270
+ padding: EdgeInsets.only(top: 0),
271
+ shrinkWrap: true,
272
+ physics: NeverScrollableScrollPhysics(),
273
+ itemCount: itemCount,
274
+ itemBuilder: (context, index) {
275
+ final item = outputs[index];
276
+ final _address = item.address;
277
+ final _amount =
278
+ item.cryptoAmount.replaceAll(',', '.');
279
+
280
+ return Column(
281
children: [
208
- Text(
209
- _amount,
210
- style: TextStyle(
211
- fontSize: 12,
212
- fontWeight: FontWeight.w600,
213
- fontFamily: 'Lato',
214
- color: PaletteDark.pigeonBlue,
215
- decoration: TextDecoration.none,
216
- ),
282
+ Padding(
283
+ padding: EdgeInsets.only(top: 8),
284
+ child: Text(
285
+ _address,
286
+ style: TextStyle(
287
+ fontSize: 12,
288
+ fontWeight: FontWeight.w600,
289
+ fontFamily: 'Lato',
290
+ color: PaletteDark.pigeonBlue,
291
+ decoration: TextDecoration.none,
292
+ ),
293
+ )
294
+ ),
295
+ Padding(
296
+ padding: EdgeInsets.only(top: 8),
297
+ child: Row(
298
+ mainAxisSize: MainAxisSize.max,
299
+ mainAxisAlignment: MainAxisAlignment.end,
300
+ children: [
301
+ Text(
302
+ _amount,
303
+ style: TextStyle(
304
+ fontSize: 12,
305
+ fontWeight: FontWeight.w600,
306
+ fontFamily: 'Lato',
307
+ color: PaletteDark.pigeonBlue,
308
+ decoration: TextDecoration.none,
309
+ ),
310
+ )
311
+ ],
312
+ )
313
)
314
],
219
- )
220
- )
221
- ],
222
- );
223
- }
224
- )
225
- : Padding(
226
- padding: EdgeInsets.only(top: 8),
227
- child: Text(
228
- outputs.first.address,
229
- style: TextStyle(
230
- fontSize: 12,
231
- fontWeight: FontWeight.w600,
232
- fontFamily: 'Lato',
233
- color: PaletteDark.pigeonBlue,
234
- decoration: TextDecoration.none,
315
+ );
316
+ })
317
+ : Padding(
318
+ padding: EdgeInsets.only(top: 8),
319
+ child: Text(
320
+ outputs.first.address,
321
+ style: TextStyle(
322
+ fontSize: 12,
323
+ fontWeight: FontWeight.w600,
324
+ fontFamily: 'Lato',
325
+ color: PaletteDark.pigeonBlue,
326
+ decoration: TextDecoration.none,
327
+ ),
328
+ ),
329
+ )
330
+ ],
331
),
236
- ),
237
- )
238
- ],
239
- ),
332
+ )
333
+ ],
334
+ )
335
)
241
- ],
242
- )
243
- )
336
+ ),
337
+ if (itemCount > 1) CakeScrollbar(
338
+ backgroundHeight: backgroundHeight,
339
+ thumbHeight: thumbHeight,
340
+ fromTop: fromTop,
341
+ rightOffset: -15
342
+ )
343
+ ]
344
);
345
}
346
}
\ No newline at end of file
lib/src/widgets/cake_scrollbar.dart
+4
-2
@@ -4,17 +4,19 @@ class CakeScrollbar extends StatelessWidget {
4
CakeScrollbar({
5
@required this.backgroundHeight,
6
@required this.thumbHeight,
7
- @required this.fromTop
7
+ @required this.fromTop,
8
+ this.rightOffset = 6
9
});
10
11
final double backgroundHeight;
12
final double thumbHeight;
13
final double fromTop;
14
+ final double rightOffset;
15
16
@override
17
Widget build(BuildContext context) {
18
return Positioned(
17
- right: 6,
19
+ right: rightOffset,
20
child: Container(
21
height: backgroundHeight,
22
width: 6,
lib/src/widgets/primary_button.dart
+18
-1
@@ -1,3 +1,4 @@
1
+import 'package:dotted_border/dotted_border.dart';
2
import 'package:flutter/cupertino.dart';
3
import 'package:flutter/material.dart';
4
@@ -8,18 +9,22 @@ class PrimaryButton extends StatelessWidget {
9
@required this.color,
10
@required this.textColor,
11
this.isDisabled = false,
12
+ this.isDottedBorder = false,
13
+ this.borderColor = Colors.black,
14
this.onDisabledPressed});
15
16
final VoidCallback onPressed;
17
final VoidCallback onDisabledPressed;
18
final Color color;
19
final Color textColor;
20
+ final Color borderColor;
21
final String text;
22
final bool isDisabled;
23
+ final bool isDottedBorder;
24
25
@override
26
Widget build(BuildContext context) {
22
- return ButtonTheme(
27
+ final content = ButtonTheme(
28
minWidth: double.infinity,
29
height: 52.0,
30
child: FlatButton(
@@ -27,6 +32,8 @@ class PrimaryButton extends StatelessWidget {
32
? (onDisabledPressed != null ? onDisabledPressed : null)
33
: onPressed,
34
color: isDisabled ? color.withOpacity(0.5) : color,
35
+ splashColor: Colors.transparent,
36
+ highlightColor: Colors.transparent,
37
disabledColor: color.withOpacity(0.5),
38
shape: RoundedRectangleBorder(
39
borderRadius: BorderRadius.circular(26.0),
@@ -40,6 +47,16 @@ class PrimaryButton extends StatelessWidget {
47
? textColor.withOpacity(0.5)
48
: textColor)),
49
));
50
+
51
+ return isDottedBorder
52
+ ? DottedBorder(
53
+ borderType: BorderType.RRect,
54
+ dashPattern: [6, 4],
55
+ color: borderColor,
56
+ strokeWidth: 2,
57
+ radius: Radius.circular(26),
58
+ child: content)
59
+ : content;
60
}
61
}
62
lib/view_model/send/send_view_model.dart
+3
-1
@@ -58,7 +58,9 @@ abstract class SendViewModelBase with Store {
58
59
@action
60
void removeOutput(Output output) {
61
- outputs.remove(output);
61
+ if (isBatchSending) {
62
+ outputs.remove(output);
63
+ }
64
}
65
66
@action
res/values/strings_de.arb
+1
-1
@@ -494,5 +494,5 @@
494
"address_detected" : "Adresse erkannt",
495
"address_from_domain" : "Sie haben die Adresse von der unaufhaltsamen Domain ${domain} erhalten",
496
497
- "add_receiver" : "Empfänger hinzufügen"
497
+ "add_receiver" : "Fügen Sie einen weiteren Empfänger hinzu (optional)"
498
}
res/values/strings_en.arb
+1
-1
@@ -494,5 +494,5 @@
494
"address_detected" : "Address detected",
495
"address_from_domain" : "You got address from unstoppable domain ${domain}",
496
497
- "add_receiver" : "Add receiver"
497
+ "add_receiver" : "Add another receiver (optional)"
498
}
\ No newline at end of file
res/values/strings_es.arb
+1
-1
@@ -494,5 +494,5 @@
494
"address_detected" : "Dirección detectada",
495
"address_from_domain" : "Tienes la dirección de unstoppable domain ${domain}",
496
497
- "add_receiver" : "Agregar receptor"
497
+ "add_receiver" : "Agregar otro receptor (opcional)"
498
}
\ No newline at end of file
res/values/strings_hi.arb
+1
-1
@@ -494,5 +494,5 @@
494
"address_detected" : "पता लग गया",
495
"address_from_domain" : "आपको अजेय डोमेन ${domain} से पता मिला है",
496
497
- "add_receiver" : "रिसीवर जोड़ें"
497
+ "add_receiver" : "एक और रिसीवर जोड़ें (वैकल्पिक)"
498
}
\ No newline at end of file
res/values/strings_hr.arb
+1
-1
@@ -494,5 +494,5 @@
494
"address_detected" : "Adresa je otkrivena",
495
"address_from_domain" : "Dobili ste adresu od unstoppable domain ${domain}",
496
497
- "add_receiver" : "Dodajte prijamnik"
497
+ "add_receiver" : "Dodajte drugi prijemnik (izborno)"
498
}
\ No newline at end of file
res/values/strings_it.arb
+1
-1
@@ -494,5 +494,5 @@
494
"address_detected" : "Indirizzo rilevato",
495
"address_from_domain" : "Hai l'indirizzo da unstoppable domain ${domain}",
496
497
- "add_receiver" : "Aggiungi ricevitore"
497
+ "add_receiver" : "Aggiungi un altro ricevitore (opzionale)"
498
}
\ No newline at end of file
res/values/strings_ja.arb
+1
-1
@@ -494,5 +494,5 @@
494
"address_detected" : "アドレスが検出されました",
495
"address_from_domain" : "あなたはからアドレスを得ました unstoppable domain ${domain}",
496
497
- "add_receiver" : "レシーバーを追加"
497
+ "add_receiver" : "別のレシーバーを追加します(オプション)"
498
}
\ No newline at end of file
res/values/strings_ko.arb
+1
-1
@@ -494,5 +494,5 @@
494
"address_detected" : "주소 감지",
495
"address_from_domain" : "주소는 unstoppable domain ${domain}",
496
497
- "add_receiver" : "수신기 추가"
497
+ "add_receiver" : "다른 수신기 추가(선택 사항)"
498
}
\ No newline at end of file
res/values/strings_nl.arb
+1
-1
@@ -494,5 +494,5 @@
494
"address_detected" : "Adres gedetecteerd",
495
"address_from_domain" : "Je adres is van unstoppable domain ${domain}",
496
497
- "add_receiver" : "Ontvanger toevoegen"
497
+ "add_receiver" : "Nog een ontvanger toevoegen (optioneel)"
498
}
\ No newline at end of file
res/values/strings_pl.arb
+1
-1
@@ -494,5 +494,5 @@
494
"address_detected" : "Wykryto adres",
495
"address_from_domain" : "Dostałeś adres od unstoppable domain ${domain}",
496
497
- "add_receiver" : "Dodaj odbiorcę"
497
+ "add_receiver" : "Dodaj kolejny odbiornik (opcjonalnie)"
498
}
\ No newline at end of file
res/values/strings_pt.arb
+1
-1
@@ -494,5 +494,5 @@
494
"address_detected" : "Endereço detectado",
495
"address_from_domain" : "Você obteve o endereço de unstoppable domain ${domain}",
496
497
- "add_receiver" : "Adicionar receptor"
497
+ "add_receiver" : "Adicione outro receptor (opcional)"
498
}
\ No newline at end of file
res/values/strings_ru.arb
+1
-1
@@ -494,5 +494,5 @@
494
"address_detected" : "Обнаружен адрес",
495
"address_from_domain" : "Вы получили адрес из unstoppable domain ${domain}",
496
497
- "add_receiver" : "Добавить получателя"
497
+ "add_receiver" : "Добавить получателя (необязательно)"
498
}
\ No newline at end of file
res/values/strings_uk.arb
+1
-1
@@ -494,5 +494,5 @@
494
"address_detected" : "Виявлено адресу",
495
"address_from_domain" : "Ви отримали адресу від unstoppable domain ${domain}",
496
497
- "add_receiver" : "Додати одержувача"
497
+ "add_receiver" : "Додати одержувача (необов'язково)"
498
}
\ No newline at end of file
res/values/strings_zh.arb
+1
-1
@@ -494,5 +494,5 @@
494
"address_detected" : "檢測到地址",
495
"address_from_domain" : "您有以下地址 unstoppable domain ${domain}",
496
497
- "add_receiver" : "添加接收器"
497
+ "add_receiver" : "添加另一個接收器(可選)"
498
}
\ No newline at end of file