neoagent 2.3.1-beta.89 → 2.3.1-beta.91

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.
Files changed (31) hide show
  1. package/.env.example +4 -0
  2. package/README.md +16 -7
  3. package/flutter_app/lib/features/location/location_service.dart +2 -4
  4. package/flutter_app/lib/main.dart +1 -0
  5. package/flutter_app/lib/main_app_shell.dart +17 -15
  6. package/flutter_app/lib/main_chat.dart +46 -42
  7. package/flutter_app/lib/main_controller.dart +6 -1
  8. package/flutter_app/lib/main_devices.dart +86 -742
  9. package/flutter_app/lib/main_integrations.dart +3 -3
  10. package/flutter_app/lib/main_settings.dart +50 -0
  11. package/flutter_app/lib/main_spacing.dart +18 -0
  12. package/flutter_app/lib/main_theme.dart +9 -0
  13. package/flutter_app/lib/main_unified.dart +3 -3
  14. package/lib/manager.js +33 -0
  15. package/package.json +1 -1
  16. package/server/db/database.js +74 -16
  17. package/server/guest_agent.js +1 -0
  18. package/server/public/.last_build_id +1 -1
  19. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  20. package/server/public/flutter_bootstrap.js +1 -1
  21. package/server/public/main.dart.js +50396 -50271
  22. package/server/services/ai/capabilityHealth.js +2 -3
  23. package/server/services/android/android_bootstrap_worker.js +18 -3
  24. package/server/services/android/controller.js +460 -2753
  25. package/server/services/runtime/backends/local-vm.js +33 -145
  26. package/server/services/runtime/docker-vm-manager.js +392 -0
  27. package/server/services/runtime/manager.js +53 -38
  28. package/server/services/runtime/settings.js +12 -10
  29. package/server/services/runtime/validation.js +4 -1
  30. package/server/utils/deployment.js +8 -2
  31. package/server/services/runtime/qemu.js +0 -1118
@@ -28,6 +28,7 @@ class _DevicesPanelState extends State<DevicesPanel> {
28
28
  late final TextEditingController _textEntryController;
29
29
  Timer? _surfaceFrameTimer;
30
30
  _DeviceSurface _surface = _DeviceSurface.browser;
31
+ _DeviceSurface? _runningSurface;
31
32
 
32
33
  @override
33
34
  void initState() {
@@ -66,6 +67,20 @@ class _DevicesPanelState extends State<DevicesPanel> {
66
67
  bool get _isBrowser => _surface == _DeviceSurface.browser;
67
68
  bool get _isDesktop => _surface == _DeviceSurface.desktop;
68
69
 
70
+ bool get _isCurrentSurfaceBusy =>
71
+ widget.controller.isRunningDeviceAction &&
72
+ (_runningSurface == null || _runningSurface == _surface);
73
+
74
+ Future<T> _runOnSurface<T>(Future<T> Function() action) async {
75
+ final surface = _surface;
76
+ if (mounted) setState(() => _runningSurface = surface);
77
+ try {
78
+ return await action();
79
+ } finally {
80
+ if (mounted) setState(() => _runningSurface = null);
81
+ }
82
+ }
83
+
69
84
  bool get _androidOnline {
70
85
  final status = widget.controller.androidRuntime;
71
86
  final devices = _jsonMapList(status['devices'], fallbackToMapValues: true);
@@ -164,18 +179,14 @@ class _DevicesPanelState extends State<DevicesPanel> {
164
179
  await widget.controller.refreshAndroidFrameRuntime();
165
180
  }
166
181
 
167
- Future<void> _switchSurface(int delta) async {
168
- final surfaces = _DeviceSurface.values;
169
- final currentIndex = surfaces.indexOf(_surface);
170
- final nextIndex = (currentIndex + delta) % surfaces.length;
171
- setState(
172
- () =>
173
- _surface = surfaces[nextIndex < 0 ? surfaces.length - 1 : nextIndex],
174
- );
182
+ Future<void> _selectSurface(_DeviceSurface surface) async {
183
+ setState(() => _surface = surface);
175
184
  await _ensurePreview();
176
185
  }
177
186
 
178
- Future<void> _openPrimary() async {
187
+ Future<void> _openPrimary() => _runOnSurface(_openPrimaryInner);
188
+
189
+ Future<void> _openPrimaryInner() async {
179
190
  final controller = widget.controller;
180
191
  if (_isBrowser) {
181
192
  await controller.navigateBrowserRuntime(
@@ -237,7 +248,9 @@ class _DevicesPanelState extends State<DevicesPanel> {
237
248
  await controller.openAndroidAppRuntime(packageName: raw);
238
249
  }
239
250
 
240
- Future<void> _sleepPrimary() async {
251
+ Future<void> _sleepPrimary() => _runOnSurface(_sleepPrimaryInner);
252
+
253
+ Future<void> _sleepPrimaryInner() async {
241
254
  final controller = widget.controller;
242
255
  if (_isBrowser) {
243
256
  if (controller.browserRuntime['launched'] != true) {
@@ -259,7 +272,7 @@ class _DevicesPanelState extends State<DevicesPanel> {
259
272
  await controller.stopAndroidRuntime();
260
273
  }
261
274
 
262
- Future<void> _sendText() async {
275
+ Future<void> _sendText() => _runOnSurface(() async {
263
276
  final text = _textEntryController.text;
264
277
  if (text.trim().isEmpty) {
265
278
  return;
@@ -274,9 +287,9 @@ class _DevicesPanelState extends State<DevicesPanel> {
274
287
  'pressEnter': true,
275
288
  });
276
289
  }
277
- }
290
+ });
278
291
 
279
- Future<void> _handleTap(Offset point) async {
292
+ Future<void> _handleTap(Offset point) => _runOnSurface(() async {
280
293
  if (_isBrowser) {
281
294
  await widget.controller.clickBrowserPointRuntime(
282
295
  x: point.dx.round(),
@@ -302,9 +315,9 @@ class _DevicesPanelState extends State<DevicesPanel> {
302
315
  'x': point.dx.round(),
303
316
  'y': point.dy.round(),
304
317
  });
305
- }
318
+ });
306
319
 
307
- Future<void> _handleSwipe(Offset start, Offset end) async {
320
+ Future<void> _handleSwipe(Offset start, Offset end) => _runOnSurface(() async {
308
321
  if (_isBrowser) {
309
322
  await widget.controller.scrollBrowserRuntime(
310
323
  deltaY: (start.dy - end.dy).round(),
@@ -333,9 +346,9 @@ class _DevicesPanelState extends State<DevicesPanel> {
333
346
  'y2': end.dy.round(),
334
347
  'durationMs': 280,
335
348
  });
336
- }
349
+ });
337
350
 
338
- Future<void> _runQuickAction(String action) async {
351
+ Future<void> _runQuickAction(String action) => _runOnSurface(() async {
339
352
  final controller = widget.controller;
340
353
  switch (action) {
341
354
  case 'browser_refresh':
@@ -369,7 +382,7 @@ class _DevicesPanelState extends State<DevicesPanel> {
369
382
  await _ensurePreview();
370
383
  break;
371
384
  }
372
- }
385
+ });
373
386
 
374
387
  @override
375
388
  Widget build(BuildContext context) {
@@ -471,7 +484,7 @@ class _DevicesPanelState extends State<DevicesPanel> {
471
484
  ),
472
485
  );
473
486
  }).toList(),
474
- onChanged: controller.isRunningDeviceAction
487
+ onChanged: _isCurrentSurfaceBusy
475
488
  ? null
476
489
  : (value) {
477
490
  if (value == null || value.isEmpty) {
@@ -533,7 +546,7 @@ class _DevicesPanelState extends State<DevicesPanel> {
533
546
  ? _desktopOnline
534
547
  : _androidOnline || _androidStarting),
535
548
  starting: !_isBrowser && !_isDesktop && _androidStarting,
536
- busy: controller.isRunningDeviceAction,
549
+ busy: _isCurrentSurfaceBusy,
537
550
  onSubmit: _openPrimary,
538
551
  onSleep: _sleepPrimary,
539
552
  ),
@@ -542,7 +555,7 @@ class _DevicesPanelState extends State<DevicesPanel> {
542
555
  surface: _surface,
543
556
  controller: controller,
544
557
  screenshotPath: _activeScreenshotPath,
545
- busy: controller.isRunningDeviceAction,
558
+ busy: _isCurrentSurfaceBusy,
546
559
  wakingUp: !_isBrowser && !_isDesktop && _androidStarting,
547
560
  enabled: _isBrowser || _isDesktop || _androidOnline,
548
561
  connectRequired: _desktopRequiresSelection,
@@ -553,7 +566,7 @@ class _DevicesPanelState extends State<DevicesPanel> {
553
566
  if (!_isBrowser && !_isDesktop) ...<Widget>[
554
567
  const SizedBox(height: 12),
555
568
  _AndroidNavDock(
556
- busy: controller.isRunningDeviceAction,
569
+ busy: _isCurrentSurfaceBusy,
557
570
  androidOnline: _androidOnline,
558
571
  onAction: _runQuickAction,
559
572
  ),
@@ -561,7 +574,7 @@ class _DevicesPanelState extends State<DevicesPanel> {
561
574
  const SizedBox(height: 14),
562
575
  AndroidApkDropZone(
563
576
  enabled: _androidOnline,
564
- busy: controller.isRunningDeviceAction,
577
+ busy: _isCurrentSurfaceBusy,
565
578
  onInstall: ({required filename, required bytes}) {
566
579
  return controller.installAndroidApkRuntime(
567
580
  filename: filename,
@@ -574,7 +587,7 @@ class _DevicesPanelState extends State<DevicesPanel> {
574
587
  const SizedBox(height: 18),
575
588
  _DeviceTypeDock(
576
589
  controller: _textEntryController,
577
- busy: controller.isRunningDeviceAction,
590
+ busy: _isCurrentSurfaceBusy,
578
591
  surface: _surface,
579
592
  onSubmit: _sendText,
580
593
  ),
@@ -583,15 +596,14 @@ class _DevicesPanelState extends State<DevicesPanel> {
583
596
  _DeviceQuickActions(
584
597
  surface: _surface,
585
598
  androidOnline: _androidOnline,
586
- busy: controller.isRunningDeviceAction,
599
+ busy: _isCurrentSurfaceBusy,
587
600
  onAction: _runQuickAction,
588
601
  ),
589
602
  ],
590
603
  const SizedBox(height: 14),
591
604
  _SurfaceSwitcher(
592
605
  surface: _surface,
593
- onPrevious: () => _switchSurface(-1),
594
- onNext: () => _switchSurface(1),
606
+ onSelect: _selectSurface,
595
607
  ),
596
608
  ],
597
609
  ),
@@ -1080,6 +1092,7 @@ class _AndroidNavDock extends StatelessWidget {
1080
1092
  final disabled =
1081
1093
  busy || (!androidOnline && entry.key.startsWith('android_'));
1082
1094
  return IconButton.filledTonal(
1095
+ tooltip: entry.key.replaceAll('_', ' '),
1083
1096
  onPressed: disabled ? null : () => onAction(entry.key),
1084
1097
  icon: Icon(entry.value),
1085
1098
  );
@@ -1092,79 +1105,54 @@ class _AndroidNavDock extends StatelessWidget {
1092
1105
  class _SurfaceSwitcher extends StatelessWidget {
1093
1106
  const _SurfaceSwitcher({
1094
1107
  required this.surface,
1095
- required this.onPrevious,
1096
- required this.onNext,
1108
+ required this.onSelect,
1097
1109
  });
1098
1110
 
1099
1111
  final _DeviceSurface surface;
1100
- final VoidCallback onPrevious;
1101
- final VoidCallback onNext;
1112
+ final Future<void> Function(_DeviceSurface) onSelect;
1102
1113
 
1103
1114
  @override
1104
1115
  Widget build(BuildContext context) {
1105
- return LayoutBuilder(
1106
- builder: (context, constraints) {
1107
- final compact = constraints.maxWidth < 520;
1108
- final labelColumn = Column(
1109
- mainAxisSize: MainAxisSize.min,
1110
- children: <Widget>[
1111
- Text(
1112
- surface.label,
1113
- textAlign: TextAlign.center,
1114
- style: TextStyle(fontSize: 16, fontWeight: FontWeight.w800),
1115
- ),
1116
- const SizedBox(height: 4),
1117
- Text(
1118
- surface.helper,
1119
- textAlign: TextAlign.center,
1120
- maxLines: compact ? 3 : 2,
1121
- overflow: TextOverflow.ellipsis,
1122
- style: TextStyle(color: _textSecondary),
1123
- ),
1124
- ],
1125
- );
1126
-
1127
- if (compact) {
1128
- return Column(
1129
- mainAxisSize: MainAxisSize.min,
1130
- children: <Widget>[
1131
- Row(
1132
- mainAxisAlignment: MainAxisAlignment.center,
1133
- children: <Widget>[
1134
- IconButton.filledTonal(
1135
- onPressed: onPrevious,
1136
- icon: Icon(Icons.arrow_back_ios_new_rounded),
1137
- ),
1138
- const SizedBox(width: 14),
1139
- Flexible(child: labelColumn),
1140
- const SizedBox(width: 14),
1141
- IconButton.filledTonal(
1142
- onPressed: onNext,
1143
- icon: Icon(Icons.arrow_forward_ios_rounded),
1144
- ),
1145
- ],
1116
+ return Column(
1117
+ mainAxisSize: MainAxisSize.min,
1118
+ children: <Widget>[
1119
+ Wrap(
1120
+ spacing: 8,
1121
+ runSpacing: 8,
1122
+ alignment: WrapAlignment.center,
1123
+ children: _DeviceSurface.values.map((s) {
1124
+ final selected = s == surface;
1125
+ return ChoiceChip(
1126
+ avatar: Icon(
1127
+ s.icon,
1128
+ size: 16,
1129
+ color: selected ? _textPrimary : _textSecondary,
1146
1130
  ),
1147
- ],
1148
- );
1149
- }
1150
-
1151
- return Row(
1152
- mainAxisAlignment: MainAxisAlignment.center,
1153
- children: <Widget>[
1154
- IconButton.filledTonal(
1155
- onPressed: onPrevious,
1156
- icon: Icon(Icons.arrow_back_ios_new_rounded),
1157
- ),
1158
- const SizedBox(width: 14),
1159
- labelColumn,
1160
- const SizedBox(width: 14),
1161
- IconButton.filledTonal(
1162
- onPressed: onNext,
1163
- icon: Icon(Icons.arrow_forward_ios_rounded),
1164
- ),
1165
- ],
1166
- );
1167
- },
1131
+ label: Text(s.label),
1132
+ selected: selected,
1133
+ onSelected: (_) => onSelect(s),
1134
+ selectedColor: _accentMuted,
1135
+ backgroundColor: _bgCard,
1136
+ side: BorderSide(
1137
+ color: selected
1138
+ ? _accent.withValues(alpha: 0.42)
1139
+ : _borderLight,
1140
+ ),
1141
+ labelStyle: TextStyle(
1142
+ color: selected ? _textPrimary : _textSecondary,
1143
+ fontWeight:
1144
+ selected ? FontWeight.w700 : FontWeight.w500,
1145
+ ),
1146
+ );
1147
+ }).toList(),
1148
+ ),
1149
+ const SizedBox(height: 8),
1150
+ Text(
1151
+ surface.helper,
1152
+ textAlign: TextAlign.center,
1153
+ style: TextStyle(color: _textSecondary),
1154
+ ),
1155
+ ],
1168
1156
  );
1169
1157
  }
1170
1158
  }
@@ -1386,7 +1374,10 @@ class _InteractiveSurfacePreviewState
1386
1374
  onPressed: widget.onWakeRequested,
1387
1375
  );
1388
1376
  }
1389
- return GestureDetector(
1377
+ return Semantics(
1378
+ button: true,
1379
+ label: 'Device surface preview — tap to interact, swipe to scroll',
1380
+ child: GestureDetector(
1390
1381
  onTapUp: widget.busy
1391
1382
  ? null
1392
1383
  : (details) async {
@@ -1462,6 +1453,7 @@ class _InteractiveSurfacePreviewState
1462
1453
  const Center(child: CircularProgressIndicator()),
1463
1454
  ],
1464
1455
  ),
1456
+ ),
1465
1457
  );
1466
1458
  },
1467
1459
  ),
@@ -1583,656 +1575,8 @@ class _EmptySurfaceState extends StatelessWidget {
1583
1575
  }
1584
1576
  }
1585
1577
 
1586
- // ignore: unused_element
1587
- class _RuntimeControlCard extends StatelessWidget {
1588
- const _RuntimeControlCard({
1589
- required this.title,
1590
- required this.subtitle,
1591
- required this.status,
1592
- required this.child,
1593
- });
1594
-
1595
- final String title;
1596
- final String subtitle;
1597
- final Widget status;
1598
- final Widget child;
1599
-
1600
- @override
1601
- Widget build(BuildContext context) {
1602
- return Card(
1603
- child: Padding(
1604
- padding: const EdgeInsets.all(20),
1605
- child: Column(
1606
- crossAxisAlignment: CrossAxisAlignment.start,
1607
- children: <Widget>[
1608
- Row(
1609
- crossAxisAlignment: CrossAxisAlignment.start,
1610
- children: <Widget>[
1611
- Expanded(
1612
- child: Column(
1613
- crossAxisAlignment: CrossAxisAlignment.start,
1614
- children: <Widget>[
1615
- Text(
1616
- title,
1617
- style: TextStyle(
1618
- fontSize: 20,
1619
- fontWeight: FontWeight.w800,
1620
- ),
1621
- ),
1622
- const SizedBox(height: 6),
1623
- Text(
1624
- subtitle,
1625
- style: TextStyle(color: _textSecondary, height: 1.5),
1626
- ),
1627
- ],
1628
- ),
1629
- ),
1630
- const SizedBox(width: 12),
1631
- status,
1632
- ],
1633
- ),
1634
- const SizedBox(height: 18),
1635
- child,
1636
- ],
1637
- ),
1638
- ),
1639
- );
1640
- }
1641
- }
1642
-
1643
- // ignore: unused_element
1644
- class _BrowserControls extends StatelessWidget {
1645
- const _BrowserControls({
1646
- required this.controller,
1647
- required this.browserStatus,
1648
- required this.browserPageInfo,
1649
- required this.urlController,
1650
- required this.waitForController,
1651
- required this.clickSelectorController,
1652
- required this.clickTextController,
1653
- required this.fillSelectorController,
1654
- required this.fillValueController,
1655
- });
1656
-
1657
- final NeoAgentController controller;
1658
- final Map<String, dynamic> browserStatus;
1659
- final Map<String, dynamic> browserPageInfo;
1660
- final TextEditingController urlController;
1661
- final TextEditingController waitForController;
1662
- final TextEditingController clickSelectorController;
1663
- final TextEditingController clickTextController;
1664
- final TextEditingController fillSelectorController;
1665
- final TextEditingController fillValueController;
1666
-
1667
- @override
1668
- Widget build(BuildContext context) {
1669
- final launched = browserStatus['launched'] == true;
1670
- return Column(
1671
- crossAxisAlignment: CrossAxisAlignment.start,
1672
- children: <Widget>[
1673
- Wrap(
1674
- spacing: 10,
1675
- runSpacing: 10,
1676
- children: <Widget>[
1677
- _MetaPill(
1678
- label: launched ? 'Launched' : 'Idle',
1679
- icon: Icons.language_outlined,
1680
- ),
1681
- _MetaPill(
1682
- label: 'Pages ${browserStatus['pages'] ?? 0}',
1683
- icon: Icons.filter_none_outlined,
1684
- ),
1685
- _MetaPill(
1686
- label: browserStatus['headless'] == false
1687
- ? 'Visible window'
1688
- : 'Headless',
1689
- icon: Icons.visibility_outlined,
1690
- ),
1691
- ],
1692
- ),
1693
- if ((browserPageInfo['url']?.toString().isNotEmpty ?? false) ||
1694
- (browserPageInfo['title']?.toString().isNotEmpty ??
1695
- false)) ...<Widget>[
1696
- const SizedBox(height: 14),
1697
- SelectableText(
1698
- '${browserPageInfo['title'] ?? 'Untitled'}\n${browserPageInfo['url'] ?? ''}',
1699
- style: TextStyle(color: _textSecondary, height: 1.5),
1700
- ),
1701
- ],
1702
- const SizedBox(height: 18),
1703
- _DeviceFieldRow(
1704
- children: <Widget>[
1705
- _DeviceField(
1706
- label: 'URL',
1707
- child: TextField(controller: urlController),
1708
- ),
1709
- _DeviceField(
1710
- label: 'Wait For Selector',
1711
- child: TextField(controller: waitForController),
1712
- ),
1713
- ],
1714
- ),
1715
- const SizedBox(height: 10),
1716
- Wrap(
1717
- spacing: 10,
1718
- runSpacing: 10,
1719
- children: <Widget>[
1720
- FilledButton.icon(
1721
- onPressed: controller.isRunningDeviceAction
1722
- ? null
1723
- : controller.launchBrowserRuntime,
1724
- icon: Icon(Icons.rocket_launch_outlined),
1725
- label: Text('Launch'),
1726
- ),
1727
- FilledButton.icon(
1728
- onPressed: controller.isRunningDeviceAction
1729
- ? null
1730
- : () => controller.navigateBrowserRuntime(
1731
- url: urlController.text.trim(),
1732
- waitFor: waitForController.text.trim(),
1733
- ),
1734
- icon: Icon(Icons.open_in_browser_outlined),
1735
- label: Text('Navigate'),
1736
- ),
1737
- OutlinedButton.icon(
1738
- onPressed: controller.isRunningDeviceAction
1739
- ? null
1740
- : controller.screenshotBrowserRuntime,
1741
- icon: Icon(Icons.photo_camera_back_outlined),
1742
- label: Text('Screenshot'),
1743
- ),
1744
- OutlinedButton.icon(
1745
- onPressed: controller.isRunningDeviceAction
1746
- ? null
1747
- : controller.closeBrowserRuntime,
1748
- icon: Icon(Icons.close),
1749
- label: Text('Close'),
1750
- ),
1751
- ],
1752
- ),
1753
- const SizedBox(height: 18),
1754
- _DeviceFieldRow(
1755
- children: <Widget>[
1756
- _DeviceField(
1757
- label: 'Click Selector',
1758
- child: TextField(controller: clickSelectorController),
1759
- ),
1760
- _DeviceField(
1761
- label: 'Click Text',
1762
- child: TextField(controller: clickTextController),
1763
- ),
1764
- ],
1765
- ),
1766
- const SizedBox(height: 10),
1767
- Wrap(
1768
- spacing: 10,
1769
- runSpacing: 10,
1770
- children: <Widget>[
1771
- OutlinedButton(
1772
- onPressed: controller.isRunningDeviceAction
1773
- ? null
1774
- : () => controller.clickBrowserRuntime(
1775
- selector: clickSelectorController.text.trim(),
1776
- ),
1777
- child: Text('Click Selector'),
1778
- ),
1779
- OutlinedButton(
1780
- onPressed: controller.isRunningDeviceAction
1781
- ? null
1782
- : () => controller.clickBrowserRuntime(
1783
- text: clickTextController.text.trim(),
1784
- ),
1785
- child: Text('Click Text'),
1786
- ),
1787
- ],
1788
- ),
1789
- const SizedBox(height: 18),
1790
- _DeviceFieldRow(
1791
- children: <Widget>[
1792
- _DeviceField(
1793
- label: 'Type Selector',
1794
- child: TextField(controller: fillSelectorController),
1795
- ),
1796
- _DeviceField(
1797
- label: 'Value',
1798
- child: TextField(controller: fillValueController),
1799
- ),
1800
- ],
1801
- ),
1802
- const SizedBox(height: 10),
1803
- OutlinedButton.icon(
1804
- onPressed: controller.isRunningDeviceAction
1805
- ? null
1806
- : () => controller.fillBrowserRuntime(
1807
- selector: fillSelectorController.text.trim(),
1808
- value: fillValueController.text,
1809
- ),
1810
- icon: Icon(Icons.keyboard_outlined),
1811
- label: Text('Type Into Field'),
1812
- ),
1813
- const SizedBox(height: 18),
1814
- _RuntimePreview(
1815
- title: 'Latest Browser Screenshot',
1816
- screenshotPath: controller.browserScreenshotPath,
1817
- controller: controller,
1818
- ),
1819
- if (controller.browserLastResult?.trim().isNotEmpty ??
1820
- false) ...<Widget>[
1821
- const SizedBox(height: 14),
1822
- _ResultBlock(
1823
- label: 'Last browser result',
1824
- value: controller.browserLastResult!,
1825
- ),
1826
- ],
1827
- ],
1828
- );
1829
- }
1830
- }
1831
-
1832
- // ignore: unused_element
1833
- class _AndroidControls extends StatelessWidget {
1834
- const _AndroidControls({
1835
- required this.controller,
1836
- required this.androidStatus,
1837
- required this.androidDevices,
1838
- required this.packageController,
1839
- required this.activityController,
1840
- required this.intentActionController,
1841
- required this.intentDataController,
1842
- required this.tapTextController,
1843
- required this.tapDescriptionController,
1844
- required this.tapResourceIdController,
1845
- required this.tapXController,
1846
- required this.tapYController,
1847
- required this.typeTextController,
1848
- required this.typeFieldTextController,
1849
- required this.typeFieldDescriptionController,
1850
- required this.waitTextController,
1851
- required this.keyController,
1852
- required this.swipeX1Controller,
1853
- required this.swipeY1Controller,
1854
- required this.swipeX2Controller,
1855
- required this.swipeY2Controller,
1856
- required this.toInt,
1857
- });
1858
1578
 
1859
- final NeoAgentController controller;
1860
- final Map<String, dynamic> androidStatus;
1861
- final List<Map<String, dynamic>> androidDevices;
1862
- final TextEditingController packageController;
1863
- final TextEditingController activityController;
1864
- final TextEditingController intentActionController;
1865
- final TextEditingController intentDataController;
1866
- final TextEditingController tapTextController;
1867
- final TextEditingController tapDescriptionController;
1868
- final TextEditingController tapResourceIdController;
1869
- final TextEditingController tapXController;
1870
- final TextEditingController tapYController;
1871
- final TextEditingController typeTextController;
1872
- final TextEditingController typeFieldTextController;
1873
- final TextEditingController typeFieldDescriptionController;
1874
- final TextEditingController waitTextController;
1875
- final TextEditingController keyController;
1876
- final TextEditingController swipeX1Controller;
1877
- final TextEditingController swipeY1Controller;
1878
- final TextEditingController swipeX2Controller;
1879
- final TextEditingController swipeY2Controller;
1880
- final int? Function(String text) toInt;
1881
1579
 
1882
- @override
1883
- Widget build(BuildContext context) {
1884
- return Column(
1885
- crossAxisAlignment: CrossAxisAlignment.start,
1886
- children: <Widget>[
1887
- Wrap(
1888
- spacing: 10,
1889
- runSpacing: 10,
1890
- children: <Widget>[
1891
- _MetaPill(
1892
- label: androidStatus['bootstrapped'] == true
1893
- ? 'SDK Ready'
1894
- : 'Bootstrap Needed',
1895
- icon: Icons.adb_outlined,
1896
- ),
1897
- _MetaPill(
1898
- label: androidStatus['serial']?.toString().isNotEmpty == true
1899
- ? androidStatus['serial'].toString()
1900
- : 'No active serial',
1901
- icon: Icons.phone_android_outlined,
1902
- ),
1903
- _MetaPill(
1904
- label: '${androidDevices.length} device(s)',
1905
- icon: Icons.devices_other_outlined,
1906
- ),
1907
- ],
1908
- ),
1909
- const SizedBox(height: 18),
1910
- Wrap(
1911
- spacing: 10,
1912
- runSpacing: 10,
1913
- children: <Widget>[
1914
- FilledButton.icon(
1915
- onPressed: controller.isRunningDeviceAction
1916
- ? null
1917
- : controller.startAndroidRuntime,
1918
- icon: Icon(Icons.play_arrow_outlined),
1919
- label: Text('Start Emulator'),
1920
- ),
1921
- OutlinedButton.icon(
1922
- onPressed: controller.isRunningDeviceAction
1923
- ? null
1924
- : controller.stopAndroidRuntime,
1925
- icon: Icon(Icons.stop_circle_outlined),
1926
- label: Text('Stop'),
1927
- ),
1928
- OutlinedButton.icon(
1929
- onPressed: controller.isRunningDeviceAction
1930
- ? null
1931
- : controller.screenshotAndroidRuntime,
1932
- icon: Icon(Icons.photo_camera_outlined),
1933
- label: Text('Screenshot'),
1934
- ),
1935
- OutlinedButton.icon(
1936
- onPressed: controller.isRunningDeviceAction
1937
- ? null
1938
- : controller.dumpAndroidUiRuntime,
1939
- icon: Icon(Icons.data_object_outlined),
1940
- label: Text('Dump UI'),
1941
- ),
1942
- OutlinedButton.icon(
1943
- onPressed: controller.isRunningDeviceAction
1944
- ? null
1945
- : controller.refreshAndroidApps,
1946
- icon: Icon(Icons.apps_outlined),
1947
- label: Text('Load Apps'),
1948
- ),
1949
- ],
1950
- ),
1951
- const SizedBox(height: 18),
1952
- _DeviceFieldRow(
1953
- children: <Widget>[
1954
- _DeviceField(
1955
- label: 'Package',
1956
- child: TextField(controller: packageController),
1957
- ),
1958
- _DeviceField(
1959
- label: 'Activity',
1960
- child: TextField(controller: activityController),
1961
- ),
1962
- ],
1963
- ),
1964
- const SizedBox(height: 10),
1965
- Wrap(
1966
- spacing: 10,
1967
- runSpacing: 10,
1968
- children: <Widget>[
1969
- OutlinedButton.icon(
1970
- onPressed: controller.isRunningDeviceAction
1971
- ? null
1972
- : () => controller.openAndroidAppRuntime(
1973
- packageName: packageController.text.trim(),
1974
- activity: activityController.text.trim(),
1975
- ),
1976
- icon: Icon(Icons.apps),
1977
- label: Text('Open App'),
1978
- ),
1979
- if (controller.androidInstalledApps.isNotEmpty)
1980
- SizedBox(width: 1, height: 1, child: Container()),
1981
- ],
1982
- ),
1983
- if (controller.androidInstalledApps.isNotEmpty) ...<Widget>[
1984
- const SizedBox(height: 8),
1985
- Wrap(
1986
- spacing: 8,
1987
- runSpacing: 8,
1988
- children: controller.androidInstalledApps.take(10).map((appId) {
1989
- return ActionChip(
1990
- label: Text(appId),
1991
- onPressed: () => packageController.text = appId,
1992
- );
1993
- }).toList(),
1994
- ),
1995
- ],
1996
- const SizedBox(height: 18),
1997
- _DeviceFieldRow(
1998
- children: <Widget>[
1999
- _DeviceField(
2000
- label: 'Intent Action',
2001
- child: TextField(controller: intentActionController),
2002
- ),
2003
- _DeviceField(
2004
- label: 'Intent Data',
2005
- child: TextField(controller: intentDataController),
2006
- ),
2007
- ],
2008
- ),
2009
- const SizedBox(height: 10),
2010
- OutlinedButton.icon(
2011
- onPressed: controller.isRunningDeviceAction
2012
- ? null
2013
- : () => controller.openAndroidIntentRuntime(
2014
- action: intentActionController.text.trim(),
2015
- dataUri: intentDataController.text.trim(),
2016
- packageName: packageController.text.trim(),
2017
- ),
2018
- icon: Icon(Icons.route_outlined),
2019
- label: Text('Open Intent'),
2020
- ),
2021
- const SizedBox(height: 18),
2022
- _DeviceFieldRow(
2023
- children: <Widget>[
2024
- _DeviceField(
2025
- label: 'Wait For Text',
2026
- child: TextField(controller: waitTextController),
2027
- ),
2028
- _DeviceField(
2029
- label: 'Key',
2030
- child: TextField(controller: keyController),
2031
- ),
2032
- ],
2033
- ),
2034
- const SizedBox(height: 10),
2035
- Wrap(
2036
- spacing: 10,
2037
- runSpacing: 10,
2038
- children: <Widget>[
2039
- OutlinedButton(
2040
- onPressed: controller.isRunningDeviceAction
2041
- ? null
2042
- : () => controller.waitForAndroidRuntime(<String, dynamic>{
2043
- 'text': waitTextController.text.trim(),
2044
- 'timeoutMs': 20000,
2045
- 'intervalMs': 1200,
2046
- }),
2047
- child: Text('Wait For UI'),
2048
- ),
2049
- OutlinedButton(
2050
- onPressed: controller.isRunningDeviceAction
2051
- ? null
2052
- : () => controller.pressAndroidKeyRuntime(
2053
- keyController.text.trim(),
2054
- ),
2055
- child: Text('Press Key'),
2056
- ),
2057
- ],
2058
- ),
2059
- const SizedBox(height: 18),
2060
- _DeviceFieldRow(
2061
- children: <Widget>[
2062
- _DeviceField(
2063
- label: 'Tap Text',
2064
- child: TextField(controller: tapTextController),
2065
- ),
2066
- _DeviceField(
2067
- label: 'Tap Description',
2068
- child: TextField(controller: tapDescriptionController),
2069
- ),
2070
- ],
2071
- ),
2072
- const SizedBox(height: 10),
2073
- _DeviceFieldRow(
2074
- children: <Widget>[
2075
- _DeviceField(
2076
- label: 'Tap Resource Id',
2077
- child: TextField(controller: tapResourceIdController),
2078
- ),
2079
- _DeviceField(
2080
- label: 'Tap X / Y',
2081
- child: Row(
2082
- children: <Widget>[
2083
- Expanded(child: TextField(controller: tapXController)),
2084
- const SizedBox(width: 8),
2085
- Expanded(child: TextField(controller: tapYController)),
2086
- ],
2087
- ),
2088
- ),
2089
- ],
2090
- ),
2091
- const SizedBox(height: 10),
2092
- OutlinedButton.icon(
2093
- onPressed: controller.isRunningDeviceAction
2094
- ? null
2095
- : () => controller.tapAndroidRuntime(<String, dynamic>{
2096
- if (tapTextController.text.trim().isNotEmpty)
2097
- 'text': tapTextController.text.trim(),
2098
- if (tapDescriptionController.text.trim().isNotEmpty)
2099
- 'description': tapDescriptionController.text.trim(),
2100
- if (tapResourceIdController.text.trim().isNotEmpty)
2101
- 'resourceId': tapResourceIdController.text.trim(),
2102
- if (toInt(tapXController.text) != null)
2103
- 'x': toInt(tapXController.text),
2104
- if (toInt(tapYController.text) != null)
2105
- 'y': toInt(tapYController.text),
2106
- }),
2107
- icon: Icon(Icons.touch_app_outlined),
2108
- label: Text('Tap'),
2109
- ),
2110
- const SizedBox(height: 18),
2111
- _DeviceFieldRow(
2112
- children: <Widget>[
2113
- _DeviceField(
2114
- label: 'Type Text',
2115
- child: TextField(controller: typeTextController),
2116
- ),
2117
- _DeviceField(
2118
- label: 'Focus Field Text / Description',
2119
- child: Row(
2120
- children: <Widget>[
2121
- Expanded(
2122
- child: TextField(controller: typeFieldTextController),
2123
- ),
2124
- const SizedBox(width: 8),
2125
- Expanded(
2126
- child: TextField(
2127
- controller: typeFieldDescriptionController,
2128
- ),
2129
- ),
2130
- ],
2131
- ),
2132
- ),
2133
- ],
2134
- ),
2135
- const SizedBox(height: 10),
2136
- OutlinedButton.icon(
2137
- onPressed: controller.isRunningDeviceAction
2138
- ? null
2139
- : () => controller.typeAndroidRuntime(<String, dynamic>{
2140
- 'text': typeTextController.text,
2141
- if (typeFieldTextController.text.trim().isNotEmpty)
2142
- 'textSelector': typeFieldTextController.text.trim(),
2143
- if (typeFieldDescriptionController.text.trim().isNotEmpty)
2144
- 'description': typeFieldDescriptionController.text.trim(),
2145
- 'pressEnter': true,
2146
- }),
2147
- icon: Icon(Icons.keyboard_outlined),
2148
- label: Text('Type'),
2149
- ),
2150
- const SizedBox(height: 18),
2151
- _DeviceFieldRow(
2152
- children: <Widget>[
2153
- _DeviceField(
2154
- label: 'Swipe X1 / Y1',
2155
- child: Row(
2156
- children: <Widget>[
2157
- Expanded(child: TextField(controller: swipeX1Controller)),
2158
- const SizedBox(width: 8),
2159
- Expanded(child: TextField(controller: swipeY1Controller)),
2160
- ],
2161
- ),
2162
- ),
2163
- _DeviceField(
2164
- label: 'Swipe X2 / Y2',
2165
- child: Row(
2166
- children: <Widget>[
2167
- Expanded(child: TextField(controller: swipeX2Controller)),
2168
- const SizedBox(width: 8),
2169
- Expanded(child: TextField(controller: swipeY2Controller)),
2170
- ],
2171
- ),
2172
- ),
2173
- ],
2174
- ),
2175
- const SizedBox(height: 10),
2176
- OutlinedButton.icon(
2177
- onPressed: controller.isRunningDeviceAction
2178
- ? null
2179
- : () => controller.swipeAndroidRuntime(<String, dynamic>{
2180
- 'x1': toInt(swipeX1Controller.text),
2181
- 'y1': toInt(swipeY1Controller.text),
2182
- 'x2': toInt(swipeX2Controller.text),
2183
- 'y2': toInt(swipeY2Controller.text),
2184
- }),
2185
- icon: Icon(Icons.swipe_outlined),
2186
- label: Text('Swipe'),
2187
- ),
2188
- const SizedBox(height: 18),
2189
- _RuntimePreview(
2190
- title: 'Latest Android Screenshot',
2191
- screenshotPath: controller.androidScreenshotPath,
2192
- controller: controller,
2193
- ),
2194
- if (controller.androidUiPreview.isNotEmpty) ...<Widget>[
2195
- const SizedBox(height: 14),
2196
- Text(
2197
- 'Latest UI dump preview',
2198
- style: TextStyle(fontWeight: FontWeight.w700),
2199
- ),
2200
- const SizedBox(height: 10),
2201
- ...controller.androidUiPreview.take(6).map((node) {
2202
- final title = node['text']?.toString().trim().isNotEmpty == true
2203
- ? node['text'].toString()
2204
- : node['description']?.toString().trim().isNotEmpty == true
2205
- ? node['description'].toString()
2206
- : node['resourceId']?.toString().trim().isNotEmpty == true
2207
- ? node['resourceId'].toString()
2208
- : node['className']?.toString() ?? 'node';
2209
- return Container(
2210
- margin: const EdgeInsets.only(bottom: 8),
2211
- padding: const EdgeInsets.all(12),
2212
- decoration: BoxDecoration(
2213
- color: _bgSecondary,
2214
- borderRadius: BorderRadius.circular(10),
2215
- border: Border.all(color: _border),
2216
- ),
2217
- child: Text(
2218
- '$title\n${node['packageName'] ?? ''}',
2219
- style: TextStyle(color: _textSecondary, height: 1.5),
2220
- ),
2221
- );
2222
- }),
2223
- ],
2224
- if (controller.androidLastResult?.trim().isNotEmpty ??
2225
- false) ...<Widget>[
2226
- const SizedBox(height: 14),
2227
- _ResultBlock(
2228
- label: 'Last Android result',
2229
- value: controller.androidLastResult!,
2230
- ),
2231
- ],
2232
- ],
2233
- );
2234
- }
2235
- }
2236
1580
 
2237
1581
  class _DeviceFieldRow extends StatelessWidget {
2238
1582
  const _DeviceFieldRow({required this.children});