1
+import 'package:cake_wallet/src/screens/base_page.dart';
2
+import 'package:cake_wallet/view_model/dev/shared_preferences.dart';
3
+import 'package:flutter/material.dart';
4
+import 'package:flutter/services.dart';
5
+import 'package:flutter_mobx/flutter_mobx.dart';
6
+
7
+class DevSharedPreferencesPage extends BasePage {
8
+ final DevSharedPreferences viewModel;
9
+
10
+ DevSharedPreferencesPage(this.viewModel);
11
+
12
+ @override
13
+ String? get title => "[dev] shared preferences";
14
+
15
+ @override
16
+ Widget? trailing(BuildContext context) {
17
+ return IconButton(
18
+ icon: Icon(Icons.add),
19
+ onPressed: () => _showCreateDialog(context),
20
+ );
21
+ }
22
+
23
+ @override
24
+ Widget body(BuildContext context) {
25
+ return Observer(
26
+ builder: (_) {
27
+ if (viewModel.sharedPreferences == null) {
28
+ return Center(child: Text("No shared preferences found"));
29
+ }
30
+ final keys = viewModel.keys;
31
+ Map<String, dynamic> values = {};
32
+ for (final key in keys) {
33
+ values[key] = viewModel.get(key);
34
+ }
35
+ Map<String, PreferenceType> types = {};
36
+ for (final key in keys) {
37
+ types[key] = viewModel.getPreferenceType(key);
38
+ }
39
+ return ListView.builder(
40
+ itemCount: keys.length,
41
+ itemBuilder: (context, index) {
42
+ final key = keys[index];
43
+ final type = types[key]!;
44
+ return ListTile(
45
+ onTap: () {
46
+ Clipboard.setData(ClipboardData(text: key + ": " + values[key].toString()));
47
+ },
48
+ onLongPress: () {
49
+ _showEditDialog(context, key, type, values[key]);
50
+ },
51
+ title: switch (type) {
52
+ PreferenceType.bool => Text(key, style: TextStyle(color: Colors.blue)),
53
+ PreferenceType.int => Text(key, style: TextStyle(color: Colors.green)),
54
+ PreferenceType.double => Text(key, style: TextStyle(color: Colors.yellow)),
55
+ PreferenceType.listString => Text(key, style: TextStyle(color: Colors.purple)),
56
+ PreferenceType.string => Text(key),
57
+ PreferenceType.unknown => Text(key),
58
+ },
59
+ subtitle: switch (type) {
60
+ PreferenceType.bool => Text("bool: ${values[key]}"),
61
+ PreferenceType.int => Text("int: ${values[key]}"),
62
+ PreferenceType.double => Text("double: ${values[key]}"),
63
+ PreferenceType.listString => values[key].isEmpty as bool ? Text("listString: []") : Text("listString:\n- ${values[key].join("\n- ")}"),
64
+ PreferenceType.string => Text("string: ${values[key]}"),
65
+ PreferenceType.unknown => Text("UNKNOWN(${values[key].runtimeType}): ${values[key]}"),
66
+ },
67
+ );
68
+ },
69
+ );
70
+ },
71
+ );
72
+ }
73
+
74
+ void _showEditDialog(BuildContext context, String key, PreferenceType type, dynamic currentValue) {
75
+ dynamic newValue = currentValue;
76
+ bool isListString = type == PreferenceType.listString;
77
+ List<String> listItems = isListString ? List<String>.from(currentValue as Iterable<dynamic>) : [];
78
+ TextEditingController textController = TextEditingController(
79
+ text: isListString ? '' : currentValue?.toString() ?? '');
80
+
81
+ showDialog(
82
+ context: context,
83
+ builder: (BuildContext context) {
84
+ return StatefulBuilder(
85
+ builder: (context, setState) {
86
+ return AlertDialog(
87
+ title: Text('Edit $key'),
88
+ content: SizedBox(
89
+ width: double.maxFinite,
90
+ height: double.maxFinite,
91
+ child: SingleChildScrollView(
92
+ child: _buildDialogContent(
93
+ type,
94
+ newValue,
95
+ listItems,
96
+ textController,
97
+ (value) => setState(() => newValue = value),
98
+ (items) => setState(() => listItems = items),
99
+ ),
100
+ ),
101
+ ),
102
+ actions: <Widget>[
103
+ TextButton(
104
+ child: Text('Delete'),
105
+ style: TextButton.styleFrom(foregroundColor: Colors.red),
106
+ onPressed: () {
107
+ _showDeleteConfirmation(context, key);
108
+ },
109
+ ),
110
+ TextButton(
111
+ child: Text('Cancel'),
112
+ onPressed: () => Navigator.of(context).pop(),
113
+ ),
114
+ TextButton(
115
+ child: Text('Save'),
116
+ onPressed: () async {
117
+ if (_validateAndUpdateValue(
118
+ context,
119
+ type,
120
+ textController,
121
+ listItems,
122
+ (value) => newValue = value
123
+ )) {
124
+ await viewModel.set(key, type, newValue);
125
+ Navigator.of(context).pop();
126
+ }
127
+ },
128
+ ),
129
+ ],
130
+ );
131
+ },
132
+ );
133
+ },
134
+ );
135
+ }
136
+
137
+ void _showDeleteConfirmation(BuildContext context, String key) {
138
+ showDialog(
139
+ context: context,
140
+ builder: (BuildContext context) {
141
+ return AlertDialog(
142
+ title: Text('Delete Preference'),
143
+ content: Text('Are you sure you want to delete "$key"?'),
144
+ actions: <Widget>[
145
+ TextButton(
146
+ child: Text('Cancel'),
147
+ onPressed: () => Navigator.of(context).pop(),
148
+ ),
149
+ TextButton(
150
+ child: Text('Delete'),
151
+ style: TextButton.styleFrom(foregroundColor: Colors.red),
152
+ onPressed: () {
153
+ viewModel.delete(key);
154
+ Navigator.of(context).pop();
155
+ Navigator.of(context).pop();
156
+ },
157
+ ),
158
+ ],
159
+ );
160
+ },
161
+ );
162
+ }
163
+
164
+ Widget _buildDialogContent(
165
+ PreferenceType type,
166
+ dynamic value,
167
+ List<String> listItems,
168
+ TextEditingController textController,
169
+ Function(dynamic) onValueChanged,
170
+ Function(List<String>) onListChanged,
171
+ ) {
172
+ return switch (type) {
173
+ PreferenceType.bool => _buildBoolEditor(value as bool, onValueChanged),
174
+ PreferenceType.int => _buildNumberEditor(textController, 'Integer value', true),
175
+ PreferenceType.double => _buildNumberEditor(textController, 'Double value', false),
176
+ PreferenceType.string => _buildTextEditor(textController),
177
+ PreferenceType.listString => _buildListEditor(listItems, textController, onListChanged),
178
+ PreferenceType.unknown => Text('Cannot edit unknown type'),
179
+ };
180
+ }
181
+
182
+ Widget _buildBoolEditor(bool value, Function(bool) onChanged) {
183
+ return CheckboxListTile(
184
+ title: Text('Value'),
185
+ value: value,
186
+ onChanged: (newValue) {
187
+ if (newValue != null) onChanged(newValue);
188
+ },
189
+ );
190
+ }
191
+
192
+ Widget _buildTextEditor(TextEditingController controller) {
193
+ return Column(
194
+ mainAxisSize: MainAxisSize.min,
195
+ children: [
196
+ TextField(
197
+ controller: controller,
198
+ decoration: InputDecoration(labelText: 'String value'),
199
+ maxLines: null,
200
+ ),
201
+ ],
202
+ );
203
+ }
204
+
205
+ Widget _buildNumberEditor(TextEditingController controller, String label, bool isInteger) {
206
+ return Column(
207
+ mainAxisSize: MainAxisSize.min,
208
+ children: [
209
+ TextField(
210
+ controller: controller,
211
+ decoration: InputDecoration(labelText: label),
212
+ keyboardType: isInteger
213
+ ? TextInputType.number
214
+ : TextInputType.numberWithOptions(decimal: true),
215
+ inputFormatters: isInteger
216
+ ? [FilteringTextInputFormatter.digitsOnly]
217
+ : [FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d*$'))],
218
+ ),
219
+ ],
220
+ );
221
+ }
222
+
223
+ Widget _buildListEditor(
224
+ List<String> items,
225
+ TextEditingController controller,
226
+ Function(List<String>) onListChanged,
227
+ ) {
228
+ return Column(
229
+ mainAxisSize: MainAxisSize.min,
230
+ children: [
231
+ SizedBox(
232
+ height: 200,
233
+ child: ReorderableListView(
234
+ shrinkWrap: true,
235
+ children: [
236
+ for (int i = 0; i < items.length; i++)
237
+ ListTile(
238
+ key: Key('$i'),
239
+ title: Text(items[i]),
240
+ trailing: IconButton(
241
+ icon: Icon(Icons.delete),
242
+ onPressed: () {
243
+ final newList = List<String>.from(items);
244
+ newList.removeAt(i);
245
+ onListChanged(newList);
246
+ },
247
+ ),
248
+ )
249
+ ],
250
+ onReorder: (int oldIndex, int newIndex) {
251
+ final newList = List<String>.from(items);
252
+ if (oldIndex < newIndex) {
253
+ newIndex -= 1;
254
+ }
255
+ final item = newList.removeAt(oldIndex);
256
+ newList.insert(newIndex, item);
257
+ onListChanged(newList);
258
+ },
259
+ ),
260
+ ),
261
+ Row(
262
+ children: [
263
+ Expanded(
264
+ child: TextField(
265
+ controller: controller,
266
+ decoration: InputDecoration(labelText: 'New item'),
267
+ ),
268
+ ),
269
+ IconButton(
270
+ icon: Icon(Icons.add),
271
+ onPressed: () {
272
+ if (controller.text.isNotEmpty) {
273
+ final newList = List<String>.from(items);
274
+ newList.add(controller.text);
275
+ onListChanged(newList);
276
+ controller.clear();
277
+ }
278
+ },
279
+ ),
280
+ ],
281
+ ),
282
+ ],
283
+ );
284
+ }
285
+
286
+ bool _validateAndUpdateValue(
287
+ BuildContext context,
288
+ PreferenceType type,
289
+ TextEditingController controller,
290
+ List<String> listItems,
291
+ Function(dynamic) setNewValue,
292
+ ) {
293
+ switch (type) {
294
+ case PreferenceType.int:
295
+ if (controller.text.isNotEmpty) {
296
+ try {
297
+ setNewValue(int.parse(controller.text));
298
+ } catch (e) {
299
+ _showErrorMessage(context, 'Invalid integer value');
300
+ return false;
301
+ }
302
+ }
303
+ break;
304
+ case PreferenceType.double:
305
+ if (controller.text.isNotEmpty) {
306
+ try {
307
+ setNewValue(double.parse(controller.text));
308
+ } catch (e) {
309
+ _showErrorMessage(context, 'Invalid double value');
310
+ return false;
311
+ }
312
+ }
313
+ break;
314
+ case PreferenceType.string:
315
+ setNewValue(controller.text);
316
+ break;
317
+ case PreferenceType.listString:
318
+ setNewValue(listItems);
319
+ break;
320
+ default:
321
+ break;
322
+ }
323
+ return true;
324
+ }
325
+
326
+ void _showErrorMessage(BuildContext context, String message) {
327
+ ScaffoldMessenger.of(context).showSnackBar(
328
+ SnackBar(content: Text(message)),
329
+ );
330
+ }
331
+
332
+ void _showCreateDialog(BuildContext context) {
333
+ PreferenceType selectedType = PreferenceType.string;
334
+ TextEditingController keyController = TextEditingController();
335
+
336
+ showDialog(
337
+ context: context,
338
+ builder: (BuildContext context) {
339
+ return StatefulBuilder(
340
+ builder: (context, setState) {
341
+ return AlertDialog(
342
+ title: Text('Create Preference'),
343
+ content: SingleChildScrollView(
344
+ child: Column(
345
+ mainAxisSize: MainAxisSize.min,
346
+ children: [
347
+ TextField(
348
+ controller: keyController,
349
+ decoration: InputDecoration(labelText: 'Preference Key'),
350
+ ),
351
+ SizedBox(height: 16),
352
+ DropdownButtonFormField<PreferenceType>(
353
+ value: selectedType,
354
+ decoration: InputDecoration(labelText: 'Type'),
355
+ items: [
356
+ DropdownMenuItem(value: PreferenceType.string, child: Text('String')),
357
+ DropdownMenuItem(value: PreferenceType.bool, child: Text('Boolean')),
358
+ DropdownMenuItem(value: PreferenceType.int, child: Text('Integer')),
359
+ DropdownMenuItem(value: PreferenceType.double, child: Text('Double')),
360
+ DropdownMenuItem(value: PreferenceType.listString, child: Text('List of Strings')),
361
+ ],
362
+ onChanged: (value) {
363
+ if (value != null) {
364
+ setState(() {
365
+ selectedType = value;
366
+ });
367
+ }
368
+ },
369
+ ),
370
+ ],
371
+ ),
372
+ ),
373
+ actions: <Widget>[
374
+ TextButton(
375
+ child: Text('Cancel'),
376
+ onPressed: () => Navigator.of(context).pop(),
377
+ ),
378
+ TextButton(
379
+ child: Text('Create'),
380
+ onPressed: () {
381
+ if (keyController.text.isEmpty) {
382
+ _showErrorMessage(context, 'Key cannot be empty');
383
+ return;
384
+ }
385
+
386
+ viewModel.set(keyController.text, selectedType, switch (selectedType) {
387
+ PreferenceType.bool => false,
388
+ PreferenceType.int => 0,
389
+ PreferenceType.double => 0.0,
390
+ PreferenceType.string => '',
391
+ PreferenceType.listString => [],
392
+ PreferenceType.unknown => null,
393
+ });
394
+ Navigator.of(context).pop();
395
+ },
396
+ ),
397
+ ],
398
+ );
399
+ },
400
+ );
401
+ },
402
+ );
403
+ }
404
+}