flutter-skill-mcp 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1167 @@
1
+ import 'dart:async';
2
+ import 'dart:convert';
3
+ import 'dart:developer' as developer;
4
+ import 'dart:ui' as ui;
5
+
6
+ import 'package:flutter/material.dart';
7
+ import 'package:flutter/rendering.dart';
8
+ import 'package:flutter/services.dart';
9
+
10
+ /// The Binding that enables Flutter Skill automation.
11
+ class FlutterSkillBinding {
12
+ static void ensureInitialized() {
13
+ if (_registered) return;
14
+ _registered = true;
15
+ _registerExtensions();
16
+ print('Flutter Skill Binding Initialized 🚀');
17
+ }
18
+
19
+ static bool _registered = false;
20
+ static final List<String> _logs = [];
21
+ static final List<Map<String, dynamic>> _errors = [];
22
+ static int _pointerCounter = 1;
23
+
24
+ static void _registerExtensions() {
25
+ // ==================== EXISTING EXTENSIONS ====================
26
+
27
+ // 1. Interactive Elements
28
+ developer.registerExtension('ext.flutter.flutter_skill.interactive', (method, parameters) async {
29
+ try {
30
+ final elements = _findInteractiveElements();
31
+ return developer.ServiceExtensionResponse.result(
32
+ jsonEncode({'type': 'Success', 'elements': elements}),
33
+ );
34
+ } catch (e, stack) {
35
+ return _errorResponse(e, stack);
36
+ }
37
+ });
38
+
39
+ // 2. Tap
40
+ developer.registerExtension('ext.flutter.flutter_skill.tap', (method, parameters) async {
41
+ try {
42
+ final key = parameters['key'];
43
+ final text = parameters['text'];
44
+ final success = await _performTap(key: key, text: text);
45
+ return developer.ServiceExtensionResponse.result(
46
+ jsonEncode({'success': success, 'message': success ? 'Tap successful' : 'Element not found'}),
47
+ );
48
+ } catch (e, stack) {
49
+ return _errorResponse(e, stack);
50
+ }
51
+ });
52
+
53
+ // 3. Enter Text
54
+ developer.registerExtension('ext.flutter.flutter_skill.enterText', (method, parameters) async {
55
+ try {
56
+ final key = parameters['key'];
57
+ final text = parameters['text'];
58
+ if (text == null) {
59
+ return developer.ServiceExtensionResponse.error(
60
+ developer.ServiceExtensionResponse.invalidParams,
61
+ 'Missing text',
62
+ );
63
+ }
64
+ final success = await _performEnterText(key: key, text: text);
65
+ return developer.ServiceExtensionResponse.result(
66
+ jsonEncode({'success': success, 'message': success ? 'Text entered' : 'TextField not found'}),
67
+ );
68
+ } catch (e, stack) {
69
+ return _errorResponse(e, stack);
70
+ }
71
+ });
72
+
73
+ // 4. Scroll
74
+ developer.registerExtension('ext.flutter.flutter_skill.scroll', (method, parameters) async {
75
+ try {
76
+ final key = parameters['key'];
77
+ final text = parameters['text'];
78
+ final success = await _performScroll(key: key, text: text);
79
+ return developer.ServiceExtensionResponse.result(
80
+ jsonEncode({'success': success, 'message': success ? 'Scroll successful' : 'Element not found'}),
81
+ );
82
+ } catch (e, stack) {
83
+ return _errorResponse(e, stack);
84
+ }
85
+ });
86
+
87
+ // ==================== UI INSPECTION EXTENSIONS ====================
88
+
89
+ // 5. Get Widget Tree
90
+ developer.registerExtension('ext.flutter.flutter_skill.getWidgetTree', (method, parameters) async {
91
+ try {
92
+ final maxDepth = int.tryParse(parameters['maxDepth'] ?? '10') ?? 10;
93
+ final tree = _getWidgetTree(maxDepth: maxDepth);
94
+ return developer.ServiceExtensionResponse.result(jsonEncode({'tree': tree}));
95
+ } catch (e, stack) {
96
+ return _errorResponse(e, stack);
97
+ }
98
+ });
99
+
100
+ // 6. Get Widget Properties
101
+ developer.registerExtension('ext.flutter.flutter_skill.getWidgetProperties', (method, parameters) async {
102
+ try {
103
+ final key = parameters['key'];
104
+ if (key == null) {
105
+ return developer.ServiceExtensionResponse.error(
106
+ developer.ServiceExtensionResponse.invalidParams,
107
+ 'Missing key',
108
+ );
109
+ }
110
+ final properties = _getWidgetProperties(key);
111
+ return developer.ServiceExtensionResponse.result(jsonEncode({'properties': properties}));
112
+ } catch (e, stack) {
113
+ return _errorResponse(e, stack);
114
+ }
115
+ });
116
+
117
+ // 7. Get Text Content
118
+ developer.registerExtension('ext.flutter.flutter_skill.getTextContent', (method, parameters) async {
119
+ try {
120
+ final textList = _getTextContent();
121
+ return developer.ServiceExtensionResponse.result(jsonEncode({'texts': textList}));
122
+ } catch (e, stack) {
123
+ return _errorResponse(e, stack);
124
+ }
125
+ });
126
+
127
+ // 8. Find By Type
128
+ developer.registerExtension('ext.flutter.flutter_skill.findByType', (method, parameters) async {
129
+ try {
130
+ final type = parameters['type'];
131
+ if (type == null) {
132
+ return developer.ServiceExtensionResponse.error(
133
+ developer.ServiceExtensionResponse.invalidParams,
134
+ 'Missing type',
135
+ );
136
+ }
137
+ final elements = _findByType(type);
138
+ return developer.ServiceExtensionResponse.result(jsonEncode({'elements': elements}));
139
+ } catch (e, stack) {
140
+ return _errorResponse(e, stack);
141
+ }
142
+ });
143
+
144
+ // ==================== MORE INTERACTIONS ====================
145
+
146
+ // 9. Long Press
147
+ developer.registerExtension('ext.flutter.flutter_skill.longPress', (method, parameters) async {
148
+ try {
149
+ final key = parameters['key'];
150
+ final text = parameters['text'];
151
+ final duration = int.tryParse(parameters['duration'] ?? '500') ?? 500;
152
+ final success = await _performLongPress(key: key, text: text, duration: duration);
153
+ return developer.ServiceExtensionResponse.result(
154
+ jsonEncode({'success': success, 'message': success ? 'Long press successful' : 'Element not found'}),
155
+ );
156
+ } catch (e, stack) {
157
+ return _errorResponse(e, stack);
158
+ }
159
+ });
160
+
161
+ // 10. Swipe
162
+ developer.registerExtension('ext.flutter.flutter_skill.swipe', (method, parameters) async {
163
+ try {
164
+ final direction = parameters['direction'] ?? 'up';
165
+ final distance = double.tryParse(parameters['distance'] ?? '300') ?? 300;
166
+ final key = parameters['key'];
167
+ final success = await _performSwipe(direction: direction, distance: distance, key: key);
168
+ return developer.ServiceExtensionResponse.result(
169
+ jsonEncode({'success': success, 'message': success ? 'Swipe successful' : 'Swipe failed'}),
170
+ );
171
+ } catch (e, stack) {
172
+ return _errorResponse(e, stack);
173
+ }
174
+ });
175
+
176
+ // 11. Drag
177
+ developer.registerExtension('ext.flutter.flutter_skill.drag', (method, parameters) async {
178
+ try {
179
+ final fromKey = parameters['fromKey'];
180
+ final toKey = parameters['toKey'];
181
+ final success = await _performDrag(fromKey: fromKey, toKey: toKey);
182
+ return developer.ServiceExtensionResponse.result(
183
+ jsonEncode({'success': success, 'message': success ? 'Drag successful' : 'Drag failed'}),
184
+ );
185
+ } catch (e, stack) {
186
+ return _errorResponse(e, stack);
187
+ }
188
+ });
189
+
190
+ // 12. Double Tap
191
+ developer.registerExtension('ext.flutter.flutter_skill.doubleTap', (method, parameters) async {
192
+ try {
193
+ final key = parameters['key'];
194
+ final text = parameters['text'];
195
+ final success = await _performDoubleTap(key: key, text: text);
196
+ return developer.ServiceExtensionResponse.result(
197
+ jsonEncode({'success': success, 'message': success ? 'Double tap successful' : 'Element not found'}),
198
+ );
199
+ } catch (e, stack) {
200
+ return _errorResponse(e, stack);
201
+ }
202
+ });
203
+
204
+ // ==================== STATE & VALIDATION ====================
205
+
206
+ // 13. Get Text Value
207
+ developer.registerExtension('ext.flutter.flutter_skill.getTextValue', (method, parameters) async {
208
+ try {
209
+ final key = parameters['key'];
210
+ if (key == null) {
211
+ return developer.ServiceExtensionResponse.error(
212
+ developer.ServiceExtensionResponse.invalidParams,
213
+ 'Missing key',
214
+ );
215
+ }
216
+ final value = _getTextFieldValue(key);
217
+ return developer.ServiceExtensionResponse.result(jsonEncode({'value': value}));
218
+ } catch (e, stack) {
219
+ return _errorResponse(e, stack);
220
+ }
221
+ });
222
+
223
+ // 14. Get Checkbox State
224
+ developer.registerExtension('ext.flutter.flutter_skill.getCheckboxState', (method, parameters) async {
225
+ try {
226
+ final key = parameters['key'];
227
+ if (key == null) {
228
+ return developer.ServiceExtensionResponse.error(
229
+ developer.ServiceExtensionResponse.invalidParams,
230
+ 'Missing key',
231
+ );
232
+ }
233
+ final state = _getCheckboxState(key);
234
+ return developer.ServiceExtensionResponse.result(jsonEncode({'checked': state}));
235
+ } catch (e, stack) {
236
+ return _errorResponse(e, stack);
237
+ }
238
+ });
239
+
240
+ // 15. Get Slider Value
241
+ developer.registerExtension('ext.flutter.flutter_skill.getSliderValue', (method, parameters) async {
242
+ try {
243
+ final key = parameters['key'];
244
+ if (key == null) {
245
+ return developer.ServiceExtensionResponse.error(
246
+ developer.ServiceExtensionResponse.invalidParams,
247
+ 'Missing key',
248
+ );
249
+ }
250
+ final value = _getSliderValue(key);
251
+ return developer.ServiceExtensionResponse.result(jsonEncode({'value': value}));
252
+ } catch (e, stack) {
253
+ return _errorResponse(e, stack);
254
+ }
255
+ });
256
+
257
+ // 16. Wait For Element
258
+ developer.registerExtension('ext.flutter.flutter_skill.waitForElement', (method, parameters) async {
259
+ try {
260
+ final key = parameters['key'];
261
+ final text = parameters['text'];
262
+ final timeout = int.tryParse(parameters['timeout'] ?? '5000') ?? 5000;
263
+ final found = await _waitForElement(key: key, text: text, timeout: timeout);
264
+ return developer.ServiceExtensionResponse.result(
265
+ jsonEncode({'found': found, 'message': found ? 'Element found' : 'Timeout waiting for element'}),
266
+ );
267
+ } catch (e, stack) {
268
+ return _errorResponse(e, stack);
269
+ }
270
+ });
271
+
272
+ // 17. Wait For Gone
273
+ developer.registerExtension('ext.flutter.flutter_skill.waitForGone', (method, parameters) async {
274
+ try {
275
+ final key = parameters['key'];
276
+ final text = parameters['text'];
277
+ final timeout = int.tryParse(parameters['timeout'] ?? '5000') ?? 5000;
278
+ final gone = await _waitForGone(key: key, text: text, timeout: timeout);
279
+ return developer.ServiceExtensionResponse.result(
280
+ jsonEncode({'gone': gone, 'message': gone ? 'Element is gone' : 'Element still present'}),
281
+ );
282
+ } catch (e, stack) {
283
+ return _errorResponse(e, stack);
284
+ }
285
+ });
286
+
287
+ // ==================== SCREENSHOT ====================
288
+
289
+ // 18. Screenshot
290
+ developer.registerExtension('ext.flutter.flutter_skill.screenshot', (method, parameters) async {
291
+ try {
292
+ final base64Image = await _takeScreenshot();
293
+ return developer.ServiceExtensionResponse.result(jsonEncode({'image': base64Image}));
294
+ } catch (e, stack) {
295
+ return _errorResponse(e, stack);
296
+ }
297
+ });
298
+
299
+ // 19. Screenshot Element
300
+ developer.registerExtension('ext.flutter.flutter_skill.screenshotElement', (method, parameters) async {
301
+ try {
302
+ final key = parameters['key'];
303
+ if (key == null) {
304
+ return developer.ServiceExtensionResponse.error(
305
+ developer.ServiceExtensionResponse.invalidParams,
306
+ 'Missing key',
307
+ );
308
+ }
309
+ final base64Image = await _takeElementScreenshot(key);
310
+ return developer.ServiceExtensionResponse.result(jsonEncode({'image': base64Image}));
311
+ } catch (e, stack) {
312
+ return _errorResponse(e, stack);
313
+ }
314
+ });
315
+
316
+ // ==================== NAVIGATION ====================
317
+
318
+ // 20. Get Current Route
319
+ developer.registerExtension('ext.flutter.flutter_skill.getCurrentRoute', (method, parameters) async {
320
+ try {
321
+ final route = _getCurrentRoute();
322
+ return developer.ServiceExtensionResponse.result(jsonEncode({'route': route}));
323
+ } catch (e, stack) {
324
+ return _errorResponse(e, stack);
325
+ }
326
+ });
327
+
328
+ // 21. Go Back
329
+ developer.registerExtension('ext.flutter.flutter_skill.goBack', (method, parameters) async {
330
+ try {
331
+ final success = _goBack();
332
+ return developer.ServiceExtensionResponse.result(
333
+ jsonEncode({'success': success, 'message': success ? 'Navigated back' : 'Cannot go back'}),
334
+ );
335
+ } catch (e, stack) {
336
+ return _errorResponse(e, stack);
337
+ }
338
+ });
339
+
340
+ // 22. Get Navigation Stack
341
+ developer.registerExtension('ext.flutter.flutter_skill.getNavigationStack', (method, parameters) async {
342
+ try {
343
+ final stack = _getNavigationStack();
344
+ return developer.ServiceExtensionResponse.result(jsonEncode({'stack': stack}));
345
+ } catch (e, stack) {
346
+ return _errorResponse(e, stack);
347
+ }
348
+ });
349
+
350
+ // ==================== DEBUG & LOGS ====================
351
+
352
+ // 23. Get Logs
353
+ developer.registerExtension('ext.flutter.flutter_skill.getLogs', (method, parameters) async {
354
+ try {
355
+ return developer.ServiceExtensionResponse.result(jsonEncode({'logs': _logs}));
356
+ } catch (e, stack) {
357
+ return _errorResponse(e, stack);
358
+ }
359
+ });
360
+
361
+ // 24. Get Errors
362
+ developer.registerExtension('ext.flutter.flutter_skill.getErrors', (method, parameters) async {
363
+ try {
364
+ return developer.ServiceExtensionResponse.result(jsonEncode({'errors': _errors}));
365
+ } catch (e, stack) {
366
+ return _errorResponse(e, stack);
367
+ }
368
+ });
369
+
370
+ // 25. Clear Logs
371
+ developer.registerExtension('ext.flutter.flutter_skill.clearLogs', (method, parameters) async {
372
+ try {
373
+ _logs.clear();
374
+ _errors.clear();
375
+ return developer.ServiceExtensionResponse.result(jsonEncode({'success': true}));
376
+ } catch (e, stack) {
377
+ return _errorResponse(e, stack);
378
+ }
379
+ });
380
+
381
+ // 26. Get Performance
382
+ developer.registerExtension('ext.flutter.flutter_skill.getPerformance', (method, parameters) async {
383
+ try {
384
+ final perf = _getPerformanceMetrics();
385
+ return developer.ServiceExtensionResponse.result(jsonEncode(perf));
386
+ } catch (e, stack) {
387
+ return _errorResponse(e, stack);
388
+ }
389
+ });
390
+
391
+ // Setup error handler
392
+ FlutterError.onError = (FlutterErrorDetails details) {
393
+ _errors.add({
394
+ 'error': details.exception.toString(),
395
+ 'stack': details.stack?.toString(),
396
+ 'library': details.library,
397
+ 'context': details.context?.toString(),
398
+ 'timestamp': DateTime.now().toIso8601String(),
399
+ });
400
+ };
401
+ }
402
+
403
+ static developer.ServiceExtensionResponse _errorResponse(Object e, StackTrace stack) {
404
+ return developer.ServiceExtensionResponse.error(
405
+ developer.ServiceExtensionResponse.extensionError,
406
+ '$e\n$stack',
407
+ );
408
+ }
409
+
410
+ // ==================== ELEMENT FINDING ====================
411
+
412
+ static Element? _findElementByKey(String key) {
413
+ Element? found;
414
+ void visit(Element element) {
415
+ if (found != null) return;
416
+ final widget = element.widget;
417
+ if (widget.key is ValueKey<String> && (widget.key as ValueKey<String>).value == key) {
418
+ found = element;
419
+ return;
420
+ }
421
+ element.visitChildren(visit);
422
+ }
423
+
424
+ final binding = WidgetsBinding.instance;
425
+ // ignore: invalid_use_of_protected_member
426
+ if (binding.rootElement != null) {
427
+ visit(binding.rootElement!);
428
+ }
429
+ return found;
430
+ }
431
+
432
+ static Element? _findElementByText(String text) {
433
+ Element? found;
434
+ void visit(Element element) {
435
+ if (found != null) return;
436
+ final widget = element.widget;
437
+ if (widget is Text && widget.data == text) {
438
+ found = element;
439
+ return;
440
+ }
441
+ if (widget is RichText && widget.text.toPlainText() == text) {
442
+ found = element;
443
+ return;
444
+ }
445
+ element.visitChildren(visit);
446
+ }
447
+
448
+ final binding = WidgetsBinding.instance;
449
+ // ignore: invalid_use_of_protected_member
450
+ if (binding.rootElement != null) {
451
+ visit(binding.rootElement!);
452
+ }
453
+ return found;
454
+ }
455
+
456
+ static Element? _findElement({String? key, String? text}) {
457
+ if (key != null) return _findElementByKey(key);
458
+ if (text != null) return _findElementByText(text);
459
+ return null;
460
+ }
461
+
462
+ // ==================== ACTIONS ====================
463
+
464
+ static Future<bool> _performTap({String? key, String? text}) async {
465
+ final element = _findElement(key: key, text: text);
466
+ if (element == null) {
467
+ _log('Element not found for tap (key: $key, text: $text)');
468
+ return false;
469
+ }
470
+
471
+ final renderObject = element.renderObject;
472
+ if (renderObject is! RenderBox || !renderObject.hasSize) {
473
+ _log('RenderObject is not a valid RenderBox');
474
+ return false;
475
+ }
476
+
477
+ final center = renderObject.localToGlobal(renderObject.size.center(Offset.zero));
478
+ _log('Tapping at $center (key: $key, text: $text)');
479
+
480
+ await _dispatchTap(center);
481
+ _log('Tap completed on (key: $key, text: $text)');
482
+ return true;
483
+ }
484
+
485
+ static Future<void> _dispatchTap(Offset position) async {
486
+ final binding = WidgetsBinding.instance;
487
+ final pointer = _pointerCounter++;
488
+
489
+ binding.handlePointerEvent(PointerDownEvent(position: position, pointer: pointer));
490
+ await Future.delayed(const Duration(milliseconds: 50));
491
+ binding.handlePointerEvent(PointerUpEvent(position: position, pointer: pointer));
492
+ await Future.delayed(const Duration(milliseconds: 100));
493
+ }
494
+
495
+ static Future<bool> _performEnterText({String? key, required String text}) async {
496
+ final element = _findElement(key: key);
497
+ if (element == null) {
498
+ _log('TextField not found (key: $key)');
499
+ return false;
500
+ }
501
+
502
+ final renderObject = element.renderObject;
503
+ if (renderObject is! RenderBox) return false;
504
+
505
+ final center = renderObject.localToGlobal(renderObject.size.center(Offset.zero));
506
+ await _dispatchTap(center);
507
+ await Future.delayed(const Duration(milliseconds: 200));
508
+
509
+ EditableTextState? editableTextState;
510
+ void findEditable(Element e) {
511
+ if (editableTextState != null) return;
512
+ if (e is StatefulElement && e.state is EditableTextState) {
513
+ editableTextState = e.state as EditableTextState;
514
+ return;
515
+ }
516
+ e.visitChildren(findEditable);
517
+ }
518
+ findEditable(element);
519
+
520
+ if (editableTextState != null) {
521
+ editableTextState!.updateEditingValue(TextEditingValue(
522
+ text: text,
523
+ selection: TextSelection.collapsed(offset: text.length),
524
+ ));
525
+ _log('Entered text "$text" (key: $key)');
526
+ return true;
527
+ }
528
+
529
+ SystemChannels.textInput.invokeMethod('TextInput.setEditingState', {
530
+ 'text': text,
531
+ 'selectionBase': text.length,
532
+ 'selectionExtent': text.length,
533
+ 'composingBase': -1,
534
+ 'composingExtent': -1,
535
+ });
536
+ _log('Text input sent via channel');
537
+ return true;
538
+ }
539
+
540
+ static Future<bool> _performScroll({String? key, String? text}) async {
541
+ final element = _findElement(key: key, text: text);
542
+ if (element == null) {
543
+ _log('Element not found for scroll (key: $key, text: $text)');
544
+ return false;
545
+ }
546
+
547
+ try {
548
+ await Scrollable.ensureVisible(element, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut);
549
+ _log('Scrolled to element (key: $key, text: $text)');
550
+ return true;
551
+ } catch (e) {
552
+ _log('Scroll failed: $e');
553
+ return false;
554
+ }
555
+ }
556
+
557
+ static Future<bool> _performLongPress({String? key, String? text, int duration = 500}) async {
558
+ final element = _findElement(key: key, text: text);
559
+ if (element == null) return false;
560
+
561
+ final renderObject = element.renderObject;
562
+ if (renderObject is! RenderBox || !renderObject.hasSize) return false;
563
+
564
+ final center = renderObject.localToGlobal(renderObject.size.center(Offset.zero));
565
+ final binding = WidgetsBinding.instance;
566
+ final pointer = _pointerCounter++;
567
+
568
+ binding.handlePointerEvent(PointerDownEvent(position: center, pointer: pointer));
569
+ await Future.delayed(Duration(milliseconds: duration));
570
+ binding.handlePointerEvent(PointerUpEvent(position: center, pointer: pointer));
571
+ await Future.delayed(const Duration(milliseconds: 100));
572
+
573
+ _log('Long press completed (key: $key, text: $text)');
574
+ return true;
575
+ }
576
+
577
+ static Future<bool> _performSwipe({required String direction, double distance = 300, String? key}) async {
578
+ final binding = WidgetsBinding.instance;
579
+ Offset start;
580
+
581
+ if (key != null) {
582
+ final element = _findElementByKey(key);
583
+ if (element == null) return false;
584
+ final renderObject = element.renderObject;
585
+ if (renderObject is! RenderBox) return false;
586
+ start = renderObject.localToGlobal(renderObject.size.center(Offset.zero));
587
+ } else {
588
+ // Use window size for global swipe
589
+ final window = binding.platformDispatcher.views.first;
590
+ final size = window.physicalSize / window.devicePixelRatio;
591
+ start = Offset(size.width / 2, size.height / 2);
592
+ }
593
+
594
+ Offset delta;
595
+ switch (direction.toLowerCase()) {
596
+ case 'up':
597
+ delta = Offset(0, -distance);
598
+ break;
599
+ case 'down':
600
+ delta = Offset(0, distance);
601
+ break;
602
+ case 'left':
603
+ delta = Offset(-distance, 0);
604
+ break;
605
+ case 'right':
606
+ delta = Offset(distance, 0);
607
+ break;
608
+ default:
609
+ return false;
610
+ }
611
+
612
+ final pointer = _pointerCounter++;
613
+ final end = start + delta;
614
+
615
+ binding.handlePointerEvent(PointerDownEvent(position: start, pointer: pointer));
616
+ await Future.delayed(const Duration(milliseconds: 16));
617
+
618
+ const steps = 10;
619
+ for (int i = 1; i <= steps; i++) {
620
+ final current = Offset.lerp(start, end, i / steps)!;
621
+ binding.handlePointerEvent(PointerMoveEvent(position: current, pointer: pointer, delta: delta / steps.toDouble()));
622
+ await Future.delayed(const Duration(milliseconds: 16));
623
+ }
624
+
625
+ binding.handlePointerEvent(PointerUpEvent(position: end, pointer: pointer));
626
+ await Future.delayed(const Duration(milliseconds: 100));
627
+
628
+ _log('Swipe $direction completed');
629
+ return true;
630
+ }
631
+
632
+ static Future<bool> _performDrag({String? fromKey, String? toKey}) async {
633
+ if (fromKey == null || toKey == null) return false;
634
+
635
+ final fromElement = _findElementByKey(fromKey);
636
+ final toElement = _findElementByKey(toKey);
637
+ if (fromElement == null || toElement == null) return false;
638
+
639
+ final fromRender = fromElement.renderObject;
640
+ final toRender = toElement.renderObject;
641
+ if (fromRender is! RenderBox || toRender is! RenderBox) return false;
642
+
643
+ final start = fromRender.localToGlobal(fromRender.size.center(Offset.zero));
644
+ final end = toRender.localToGlobal(toRender.size.center(Offset.zero));
645
+
646
+ final binding = WidgetsBinding.instance;
647
+ final pointer = _pointerCounter++;
648
+
649
+ binding.handlePointerEvent(PointerDownEvent(position: start, pointer: pointer));
650
+ await Future.delayed(const Duration(milliseconds: 100));
651
+
652
+ const steps = 20;
653
+ for (int i = 1; i <= steps; i++) {
654
+ final current = Offset.lerp(start, end, i / steps)!;
655
+ binding.handlePointerEvent(PointerMoveEvent(position: current, pointer: pointer));
656
+ await Future.delayed(const Duration(milliseconds: 16));
657
+ }
658
+
659
+ binding.handlePointerEvent(PointerUpEvent(position: end, pointer: pointer));
660
+ await Future.delayed(const Duration(milliseconds: 100));
661
+
662
+ _log('Drag from $fromKey to $toKey completed');
663
+ return true;
664
+ }
665
+
666
+ static Future<bool> _performDoubleTap({String? key, String? text}) async {
667
+ final element = _findElement(key: key, text: text);
668
+ if (element == null) return false;
669
+
670
+ final renderObject = element.renderObject;
671
+ if (renderObject is! RenderBox || !renderObject.hasSize) return false;
672
+
673
+ final center = renderObject.localToGlobal(renderObject.size.center(Offset.zero));
674
+
675
+ await _dispatchTap(center);
676
+ await Future.delayed(const Duration(milliseconds: 50));
677
+ await _dispatchTap(center);
678
+
679
+ _log('Double tap completed (key: $key, text: $text)');
680
+ return true;
681
+ }
682
+
683
+ // ==================== UI INSPECTION ====================
684
+
685
+ static List<Map<String, dynamic>> _findInteractiveElements() {
686
+ final results = <Map<String, dynamic>>[];
687
+
688
+ void visit(Element element) {
689
+ final widget = element.widget;
690
+ String? type;
691
+ String? text;
692
+ String? key;
693
+
694
+ if (widget.key is ValueKey<String>) {
695
+ key = (widget.key as ValueKey<String>).value;
696
+ }
697
+
698
+ if (widget is ElevatedButton || widget is TextButton || widget is OutlinedButton ||
699
+ widget is IconButton || widget is FloatingActionButton) {
700
+ type = 'Button';
701
+ text = _extractTextFrom(element);
702
+ } else if (widget is TextField || widget is TextFormField) {
703
+ type = 'TextField';
704
+ } else if (widget is Checkbox) {
705
+ type = 'Checkbox';
706
+ } else if (widget is Switch) {
707
+ type = 'Switch';
708
+ } else if (widget is Slider) {
709
+ type = 'Slider';
710
+ } else if (widget is DropdownButton) {
711
+ type = 'Dropdown';
712
+ } else if (widget is InkWell && widget.onTap != null) {
713
+ type = 'Tappable';
714
+ } else if (widget is GestureDetector && widget.onTap != null) {
715
+ type = 'Tappable';
716
+ }
717
+
718
+ if (type != null) {
719
+ results.add({
720
+ if (key != null) 'key': key,
721
+ if (text != null) 'text': text,
722
+ 'type': type,
723
+ });
724
+ }
725
+
726
+ element.visitChildren(visit);
727
+ }
728
+
729
+ final binding = WidgetsBinding.instance;
730
+ // ignore: invalid_use_of_protected_member
731
+ if (binding.rootElement != null) {
732
+ visit(binding.rootElement!);
733
+ }
734
+
735
+ return results;
736
+ }
737
+
738
+ static Map<String, dynamic> _getWidgetTree({int maxDepth = 10}) {
739
+ Map<String, dynamic> buildNode(Element element, int depth) {
740
+ final widget = element.widget;
741
+ final node = <String, dynamic>{
742
+ 'type': widget.runtimeType.toString(),
743
+ };
744
+
745
+ if (widget.key is ValueKey<String>) {
746
+ node['key'] = (widget.key as ValueKey<String>).value;
747
+ }
748
+
749
+ if (widget is Text) {
750
+ node['text'] = widget.data;
751
+ }
752
+
753
+ final renderObject = element.renderObject;
754
+ if (renderObject is RenderBox && renderObject.hasSize) {
755
+ node['size'] = {'width': renderObject.size.width, 'height': renderObject.size.height};
756
+ final offset = renderObject.localToGlobal(Offset.zero);
757
+ node['position'] = {'x': offset.dx, 'y': offset.dy};
758
+ }
759
+
760
+ if (depth < maxDepth) {
761
+ final children = <Map<String, dynamic>>[];
762
+ element.visitChildren((child) {
763
+ children.add(buildNode(child, depth + 1));
764
+ });
765
+ if (children.isNotEmpty) {
766
+ node['children'] = children;
767
+ }
768
+ }
769
+
770
+ return node;
771
+ }
772
+
773
+ final binding = WidgetsBinding.instance;
774
+ // ignore: invalid_use_of_protected_member
775
+ if (binding.rootElement != null) {
776
+ return buildNode(binding.rootElement!, 0);
777
+ }
778
+ return {};
779
+ }
780
+
781
+ static Map<String, dynamic>? _getWidgetProperties(String key) {
782
+ final element = _findElementByKey(key);
783
+ if (element == null) return null;
784
+
785
+ final widget = element.widget;
786
+ final props = <String, dynamic>{
787
+ 'type': widget.runtimeType.toString(),
788
+ 'key': key,
789
+ };
790
+
791
+ final renderObject = element.renderObject;
792
+ if (renderObject is RenderBox && renderObject.hasSize) {
793
+ props['size'] = {'width': renderObject.size.width, 'height': renderObject.size.height};
794
+ final offset = renderObject.localToGlobal(Offset.zero);
795
+ props['position'] = {'x': offset.dx, 'y': offset.dy};
796
+ props['visible'] = renderObject.attached && renderObject.size.width > 0 && renderObject.size.height > 0;
797
+ }
798
+
799
+ if (widget is Text) {
800
+ props['text'] = widget.data;
801
+ props['style'] = widget.style?.toString();
802
+ } else if (widget is Container) {
803
+ props['color'] = widget.color?.toString();
804
+ props['padding'] = widget.padding?.toString();
805
+ props['margin'] = widget.margin?.toString();
806
+ }
807
+
808
+ return props;
809
+ }
810
+
811
+ static List<Map<String, dynamic>> _getTextContent() {
812
+ final results = <Map<String, dynamic>>[];
813
+
814
+ void visit(Element element) {
815
+ final widget = element.widget;
816
+ String? key;
817
+ if (widget.key is ValueKey<String>) {
818
+ key = (widget.key as ValueKey<String>).value;
819
+ }
820
+
821
+ if (widget is Text && widget.data != null) {
822
+ results.add({
823
+ 'text': widget.data,
824
+ if (key != null) 'key': key,
825
+ 'type': 'Text',
826
+ });
827
+ } else if (widget is RichText) {
828
+ results.add({
829
+ 'text': widget.text.toPlainText(),
830
+ if (key != null) 'key': key,
831
+ 'type': 'RichText',
832
+ });
833
+ }
834
+
835
+ element.visitChildren(visit);
836
+ }
837
+
838
+ final binding = WidgetsBinding.instance;
839
+ // ignore: invalid_use_of_protected_member
840
+ if (binding.rootElement != null) {
841
+ visit(binding.rootElement!);
842
+ }
843
+
844
+ return results;
845
+ }
846
+
847
+ static List<Map<String, dynamic>> _findByType(String typeName) {
848
+ final results = <Map<String, dynamic>>[];
849
+
850
+ void visit(Element element) {
851
+ final widget = element.widget;
852
+ final type = widget.runtimeType.toString();
853
+
854
+ if (type.toLowerCase().contains(typeName.toLowerCase())) {
855
+ String? key;
856
+ if (widget.key is ValueKey<String>) {
857
+ key = (widget.key as ValueKey<String>).value;
858
+ }
859
+
860
+ final node = <String, dynamic>{'type': type};
861
+ if (key != null) node['key'] = key;
862
+
863
+ final renderObject = element.renderObject;
864
+ if (renderObject is RenderBox && renderObject.hasSize) {
865
+ final offset = renderObject.localToGlobal(Offset.zero);
866
+ node['position'] = {'x': offset.dx, 'y': offset.dy};
867
+ node['size'] = {'width': renderObject.size.width, 'height': renderObject.size.height};
868
+ }
869
+
870
+ results.add(node);
871
+ }
872
+
873
+ element.visitChildren(visit);
874
+ }
875
+
876
+ final binding = WidgetsBinding.instance;
877
+ // ignore: invalid_use_of_protected_member
878
+ if (binding.rootElement != null) {
879
+ visit(binding.rootElement!);
880
+ }
881
+
882
+ return results;
883
+ }
884
+
885
+ // ==================== STATE & VALIDATION ====================
886
+
887
+ static String? _getTextFieldValue(String key) {
888
+ final element = _findElementByKey(key);
889
+ if (element == null) return null;
890
+
891
+ EditableTextState? editableTextState;
892
+ void findEditable(Element e) {
893
+ if (editableTextState != null) return;
894
+ if (e is StatefulElement && e.state is EditableTextState) {
895
+ editableTextState = e.state as EditableTextState;
896
+ return;
897
+ }
898
+ e.visitChildren(findEditable);
899
+ }
900
+ findEditable(element);
901
+
902
+ return editableTextState?.textEditingValue.text;
903
+ }
904
+
905
+ static bool? _getCheckboxState(String key) {
906
+ final element = _findElementByKey(key);
907
+ if (element == null) return null;
908
+
909
+ final widget = element.widget;
910
+ if (widget is Checkbox) {
911
+ return widget.value;
912
+ }
913
+ if (widget is Switch) {
914
+ return widget.value;
915
+ }
916
+ return null;
917
+ }
918
+
919
+ static double? _getSliderValue(String key) {
920
+ final element = _findElementByKey(key);
921
+ if (element == null) return null;
922
+
923
+ final widget = element.widget;
924
+ if (widget is Slider) {
925
+ return widget.value;
926
+ }
927
+ return null;
928
+ }
929
+
930
+ static Future<bool> _waitForElement({String? key, String? text, int timeout = 5000}) async {
931
+ final endTime = DateTime.now().add(Duration(milliseconds: timeout));
932
+
933
+ while (DateTime.now().isBefore(endTime)) {
934
+ final element = _findElement(key: key, text: text);
935
+ if (element != null) return true;
936
+ await Future.delayed(const Duration(milliseconds: 100));
937
+ }
938
+
939
+ return false;
940
+ }
941
+
942
+ static Future<bool> _waitForGone({String? key, String? text, int timeout = 5000}) async {
943
+ final endTime = DateTime.now().add(Duration(milliseconds: timeout));
944
+
945
+ while (DateTime.now().isBefore(endTime)) {
946
+ final element = _findElement(key: key, text: text);
947
+ if (element == null) return true;
948
+ await Future.delayed(const Duration(milliseconds: 100));
949
+ }
950
+
951
+ return false;
952
+ }
953
+
954
+ // ==================== SCREENSHOT ====================
955
+
956
+ static Future<String?> _takeScreenshot() async {
957
+ try {
958
+ final binding = WidgetsBinding.instance;
959
+ // ignore: invalid_use_of_protected_member
960
+ final renderObject = binding.rootElement?.renderObject;
961
+ if (renderObject is! RenderRepaintBoundary) {
962
+ // Try to find a RenderRepaintBoundary
963
+ RenderRepaintBoundary? boundary;
964
+ void findBoundary(RenderObject obj) {
965
+ if (boundary != null) return;
966
+ if (obj is RenderRepaintBoundary) {
967
+ boundary = obj;
968
+ return;
969
+ }
970
+ obj.visitChildren(findBoundary);
971
+ }
972
+ renderObject?.visitChildren(findBoundary);
973
+
974
+ if (boundary == null) {
975
+ _log('No RenderRepaintBoundary found');
976
+ return null;
977
+ }
978
+
979
+ final image = await boundary!.toImage(pixelRatio: 1.0);
980
+ final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
981
+ if (byteData == null) return null;
982
+ return base64Encode(byteData.buffer.asUint8List());
983
+ }
984
+
985
+ final image = await renderObject.toImage(pixelRatio: 1.0);
986
+ final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
987
+ if (byteData == null) return null;
988
+ return base64Encode(byteData.buffer.asUint8List());
989
+ } catch (e) {
990
+ _log('Screenshot failed: $e');
991
+ return null;
992
+ }
993
+ }
994
+
995
+ static Future<String?> _takeElementScreenshot(String key) async {
996
+ try {
997
+ final element = _findElementByKey(key);
998
+ if (element == null) {
999
+ _log('Element not found: $key');
1000
+ return null;
1001
+ }
1002
+
1003
+ final renderObject = element.renderObject;
1004
+ if (renderObject == null) {
1005
+ _log('No render object for element: $key');
1006
+ return null;
1007
+ }
1008
+
1009
+ // If it's a RenderRepaintBoundary, capture directly
1010
+ if (renderObject is RenderRepaintBoundary) {
1011
+ final image = await renderObject.toImage(pixelRatio: 1.0);
1012
+ final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
1013
+ if (byteData == null) return null;
1014
+ return base64Encode(byteData.buffer.asUint8List());
1015
+ }
1016
+
1017
+ // For other render objects, find the nearest RepaintBoundary ancestor
1018
+ RenderObject? current = renderObject;
1019
+ while (current != null) {
1020
+ if (current is RenderRepaintBoundary) {
1021
+ // Get the element's bounds relative to the boundary
1022
+ if (renderObject is RenderBox) {
1023
+ final box = renderObject;
1024
+ final boundaryBox = current;
1025
+
1026
+ // Get element position relative to boundary
1027
+ final offset = box.localToGlobal(Offset.zero, ancestor: boundaryBox);
1028
+ final size = box.size;
1029
+
1030
+ // Capture the boundary
1031
+ final fullImage = await current.toImage(pixelRatio: 1.0);
1032
+
1033
+ // Crop to element bounds
1034
+ final recorder = ui.PictureRecorder();
1035
+ final canvas = Canvas(recorder);
1036
+
1037
+ // Draw the cropped portion
1038
+ canvas.drawImageRect(
1039
+ fullImage,
1040
+ Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height),
1041
+ Rect.fromLTWH(0, 0, size.width, size.height),
1042
+ Paint(),
1043
+ );
1044
+
1045
+ final picture = recorder.endRecording();
1046
+ final croppedImage = await picture.toImage(size.width.toInt(), size.height.toInt());
1047
+ final byteData = await croppedImage.toByteData(format: ui.ImageByteFormat.png);
1048
+ if (byteData == null) return null;
1049
+ return base64Encode(byteData.buffer.asUint8List());
1050
+ }
1051
+ }
1052
+ current = current.parent;
1053
+ }
1054
+
1055
+ _log('No suitable RenderRepaintBoundary ancestor found for: $key');
1056
+ return null;
1057
+ } catch (e) {
1058
+ _log('Element screenshot failed: $e');
1059
+ return null;
1060
+ }
1061
+ }
1062
+
1063
+ // ==================== NAVIGATION ====================
1064
+
1065
+ static String? _getCurrentRoute() {
1066
+ String? currentRoute;
1067
+ final binding = WidgetsBinding.instance;
1068
+
1069
+ void visit(Element element) {
1070
+ if (element.widget is ModalRoute) {
1071
+ final route = ModalRoute.of(element);
1072
+ if (route != null) {
1073
+ currentRoute = route.settings.name;
1074
+ }
1075
+ }
1076
+ element.visitChildren(visit);
1077
+ }
1078
+
1079
+ // ignore: invalid_use_of_protected_member
1080
+ if (binding.rootElement != null) {
1081
+ visit(binding.rootElement!);
1082
+ }
1083
+
1084
+ return currentRoute;
1085
+ }
1086
+
1087
+ static bool _goBack() {
1088
+ final context = _findNavigatorContext();
1089
+ if (context == null) return false;
1090
+
1091
+ final navigator = Navigator.of(context, rootNavigator: false);
1092
+ if (navigator.canPop()) {
1093
+ navigator.pop();
1094
+ return true;
1095
+ }
1096
+ return false;
1097
+ }
1098
+
1099
+ static List<String> _getNavigationStack() {
1100
+ final routes = <String>[];
1101
+ final context = _findNavigatorContext();
1102
+ if (context == null) return routes;
1103
+
1104
+ // This is a simplified version - full implementation would need NavigatorState access
1105
+ final currentRoute = _getCurrentRoute();
1106
+ if (currentRoute != null) {
1107
+ routes.add(currentRoute);
1108
+ }
1109
+
1110
+ return routes;
1111
+ }
1112
+
1113
+ static BuildContext? _findNavigatorContext() {
1114
+ BuildContext? context;
1115
+ final binding = WidgetsBinding.instance;
1116
+
1117
+ void visit(Element element) {
1118
+ if (context != null) return;
1119
+ if (element.widget is Navigator) {
1120
+ context = element;
1121
+ return;
1122
+ }
1123
+ element.visitChildren(visit);
1124
+ }
1125
+
1126
+ // ignore: invalid_use_of_protected_member
1127
+ if (binding.rootElement != null) {
1128
+ visit(binding.rootElement!);
1129
+ }
1130
+
1131
+ return context;
1132
+ }
1133
+
1134
+ // ==================== DEBUG & LOGS ====================
1135
+
1136
+ static void _log(String message) {
1137
+ final logEntry = '[${DateTime.now().toIso8601String()}] $message';
1138
+ _logs.add(logEntry);
1139
+ print('Flutter Skill: $message');
1140
+ }
1141
+
1142
+ static Map<String, dynamic> _getPerformanceMetrics() {
1143
+ return {
1144
+ 'timestamp': DateTime.now().toIso8601String(),
1145
+ 'logCount': _logs.length,
1146
+ 'errorCount': _errors.length,
1147
+ };
1148
+ }
1149
+
1150
+ // ==================== HELPERS ====================
1151
+
1152
+ static String? _extractTextFrom(Element element) {
1153
+ String? found;
1154
+ void visit(Element e) {
1155
+ if (found != null) return;
1156
+ if (e.widget is Text) {
1157
+ found = (e.widget as Text).data;
1158
+ } else if (e.widget is RichText) {
1159
+ found = (e.widget as RichText).text.toPlainText();
1160
+ }
1161
+ e.visitChildren(visit);
1162
+ }
1163
+
1164
+ visit(element);
1165
+ return found;
1166
+ }
1167
+ }