neoagent 2.3.1-beta.90 → 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.
@@ -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
  ),
@@ -1093,83 +1105,54 @@ class _AndroidNavDock extends StatelessWidget {
1093
1105
  class _SurfaceSwitcher extends StatelessWidget {
1094
1106
  const _SurfaceSwitcher({
1095
1107
  required this.surface,
1096
- required this.onPrevious,
1097
- required this.onNext,
1108
+ required this.onSelect,
1098
1109
  });
1099
1110
 
1100
1111
  final _DeviceSurface surface;
1101
- final VoidCallback onPrevious;
1102
- final VoidCallback onNext;
1112
+ final Future<void> Function(_DeviceSurface) onSelect;
1103
1113
 
1104
1114
  @override
1105
1115
  Widget build(BuildContext context) {
1106
- return LayoutBuilder(
1107
- builder: (context, constraints) {
1108
- final compact = constraints.maxWidth < 520;
1109
- final labelColumn = Column(
1110
- mainAxisSize: MainAxisSize.min,
1111
- children: <Widget>[
1112
- Text(
1113
- surface.label,
1114
- textAlign: TextAlign.center,
1115
- style: TextStyle(fontSize: 16, fontWeight: FontWeight.w800),
1116
- ),
1117
- const SizedBox(height: 4),
1118
- Text(
1119
- surface.helper,
1120
- textAlign: TextAlign.center,
1121
- maxLines: compact ? 3 : 2,
1122
- overflow: TextOverflow.ellipsis,
1123
- style: TextStyle(color: _textSecondary),
1124
- ),
1125
- ],
1126
- );
1127
-
1128
- if (compact) {
1129
- return Column(
1130
- mainAxisSize: MainAxisSize.min,
1131
- children: <Widget>[
1132
- Row(
1133
- mainAxisAlignment: MainAxisAlignment.center,
1134
- children: <Widget>[
1135
- IconButton.filledTonal(
1136
- tooltip: 'Previous surface',
1137
- onPressed: onPrevious,
1138
- icon: Icon(Icons.arrow_back_ios_new_rounded),
1139
- ),
1140
- const SizedBox(width: 14),
1141
- Flexible(child: labelColumn),
1142
- const SizedBox(width: 14),
1143
- IconButton.filledTonal(
1144
- tooltip: 'Next surface',
1145
- onPressed: onNext,
1146
- icon: Icon(Icons.arrow_forward_ios_rounded),
1147
- ),
1148
- ],
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,
1149
1130
  ),
1150
- ],
1151
- );
1152
- }
1153
-
1154
- return Row(
1155
- mainAxisAlignment: MainAxisAlignment.center,
1156
- children: <Widget>[
1157
- IconButton.filledTonal(
1158
- tooltip: 'Previous surface',
1159
- onPressed: onPrevious,
1160
- icon: Icon(Icons.arrow_back_ios_new_rounded),
1161
- ),
1162
- const SizedBox(width: 14),
1163
- labelColumn,
1164
- const SizedBox(width: 14),
1165
- IconButton.filledTonal(
1166
- tooltip: 'Next surface',
1167
- onPressed: onNext,
1168
- icon: Icon(Icons.arrow_forward_ios_rounded),
1169
- ),
1170
- ],
1171
- );
1172
- },
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
+ ],
1173
1156
  );
1174
1157
  }
1175
1158
  }
@@ -1592,656 +1575,8 @@ class _EmptySurfaceState extends StatelessWidget {
1592
1575
  }
1593
1576
  }
1594
1577
 
1595
- // ignore: unused_element
1596
- class _RuntimeControlCard extends StatelessWidget {
1597
- const _RuntimeControlCard({
1598
- required this.title,
1599
- required this.subtitle,
1600
- required this.status,
1601
- required this.child,
1602
- });
1603
-
1604
- final String title;
1605
- final String subtitle;
1606
- final Widget status;
1607
- final Widget child;
1608
-
1609
- @override
1610
- Widget build(BuildContext context) {
1611
- return Card(
1612
- child: Padding(
1613
- padding: const EdgeInsets.all(20),
1614
- child: Column(
1615
- crossAxisAlignment: CrossAxisAlignment.start,
1616
- children: <Widget>[
1617
- Row(
1618
- crossAxisAlignment: CrossAxisAlignment.start,
1619
- children: <Widget>[
1620
- Expanded(
1621
- child: Column(
1622
- crossAxisAlignment: CrossAxisAlignment.start,
1623
- children: <Widget>[
1624
- Text(
1625
- title,
1626
- style: TextStyle(
1627
- fontSize: 20,
1628
- fontWeight: FontWeight.w800,
1629
- ),
1630
- ),
1631
- const SizedBox(height: 6),
1632
- Text(
1633
- subtitle,
1634
- style: TextStyle(color: _textSecondary, height: 1.5),
1635
- ),
1636
- ],
1637
- ),
1638
- ),
1639
- const SizedBox(width: 12),
1640
- status,
1641
- ],
1642
- ),
1643
- const SizedBox(height: 18),
1644
- child,
1645
- ],
1646
- ),
1647
- ),
1648
- );
1649
- }
1650
- }
1651
-
1652
- // ignore: unused_element
1653
- class _BrowserControls extends StatelessWidget {
1654
- const _BrowserControls({
1655
- required this.controller,
1656
- required this.browserStatus,
1657
- required this.browserPageInfo,
1658
- required this.urlController,
1659
- required this.waitForController,
1660
- required this.clickSelectorController,
1661
- required this.clickTextController,
1662
- required this.fillSelectorController,
1663
- required this.fillValueController,
1664
- });
1665
1578
 
1666
- final NeoAgentController controller;
1667
- final Map<String, dynamic> browserStatus;
1668
- final Map<String, dynamic> browserPageInfo;
1669
- final TextEditingController urlController;
1670
- final TextEditingController waitForController;
1671
- final TextEditingController clickSelectorController;
1672
- final TextEditingController clickTextController;
1673
- final TextEditingController fillSelectorController;
1674
- final TextEditingController fillValueController;
1675
1579
 
1676
- @override
1677
- Widget build(BuildContext context) {
1678
- final launched = browserStatus['launched'] == true;
1679
- return Column(
1680
- crossAxisAlignment: CrossAxisAlignment.start,
1681
- children: <Widget>[
1682
- Wrap(
1683
- spacing: 10,
1684
- runSpacing: 10,
1685
- children: <Widget>[
1686
- _MetaPill(
1687
- label: launched ? 'Launched' : 'Idle',
1688
- icon: Icons.language_outlined,
1689
- ),
1690
- _MetaPill(
1691
- label: 'Pages ${browserStatus['pages'] ?? 0}',
1692
- icon: Icons.filter_none_outlined,
1693
- ),
1694
- _MetaPill(
1695
- label: browserStatus['headless'] == false
1696
- ? 'Visible window'
1697
- : 'Headless',
1698
- icon: Icons.visibility_outlined,
1699
- ),
1700
- ],
1701
- ),
1702
- if ((browserPageInfo['url']?.toString().isNotEmpty ?? false) ||
1703
- (browserPageInfo['title']?.toString().isNotEmpty ??
1704
- false)) ...<Widget>[
1705
- const SizedBox(height: 14),
1706
- SelectableText(
1707
- '${browserPageInfo['title'] ?? 'Untitled'}\n${browserPageInfo['url'] ?? ''}',
1708
- style: TextStyle(color: _textSecondary, height: 1.5),
1709
- ),
1710
- ],
1711
- const SizedBox(height: 18),
1712
- _DeviceFieldRow(
1713
- children: <Widget>[
1714
- _DeviceField(
1715
- label: 'URL',
1716
- child: TextField(controller: urlController),
1717
- ),
1718
- _DeviceField(
1719
- label: 'Wait For Selector',
1720
- child: TextField(controller: waitForController),
1721
- ),
1722
- ],
1723
- ),
1724
- const SizedBox(height: 10),
1725
- Wrap(
1726
- spacing: 10,
1727
- runSpacing: 10,
1728
- children: <Widget>[
1729
- FilledButton.icon(
1730
- onPressed: controller.isRunningDeviceAction
1731
- ? null
1732
- : controller.launchBrowserRuntime,
1733
- icon: Icon(Icons.rocket_launch_outlined),
1734
- label: Text('Launch'),
1735
- ),
1736
- FilledButton.icon(
1737
- onPressed: controller.isRunningDeviceAction
1738
- ? null
1739
- : () => controller.navigateBrowserRuntime(
1740
- url: urlController.text.trim(),
1741
- waitFor: waitForController.text.trim(),
1742
- ),
1743
- icon: Icon(Icons.open_in_browser_outlined),
1744
- label: Text('Navigate'),
1745
- ),
1746
- OutlinedButton.icon(
1747
- onPressed: controller.isRunningDeviceAction
1748
- ? null
1749
- : controller.screenshotBrowserRuntime,
1750
- icon: Icon(Icons.photo_camera_back_outlined),
1751
- label: Text('Screenshot'),
1752
- ),
1753
- OutlinedButton.icon(
1754
- onPressed: controller.isRunningDeviceAction
1755
- ? null
1756
- : controller.closeBrowserRuntime,
1757
- icon: Icon(Icons.close),
1758
- label: Text('Close'),
1759
- ),
1760
- ],
1761
- ),
1762
- const SizedBox(height: 18),
1763
- _DeviceFieldRow(
1764
- children: <Widget>[
1765
- _DeviceField(
1766
- label: 'Click Selector',
1767
- child: TextField(controller: clickSelectorController),
1768
- ),
1769
- _DeviceField(
1770
- label: 'Click Text',
1771
- child: TextField(controller: clickTextController),
1772
- ),
1773
- ],
1774
- ),
1775
- const SizedBox(height: 10),
1776
- Wrap(
1777
- spacing: 10,
1778
- runSpacing: 10,
1779
- children: <Widget>[
1780
- OutlinedButton(
1781
- onPressed: controller.isRunningDeviceAction
1782
- ? null
1783
- : () => controller.clickBrowserRuntime(
1784
- selector: clickSelectorController.text.trim(),
1785
- ),
1786
- child: Text('Click Selector'),
1787
- ),
1788
- OutlinedButton(
1789
- onPressed: controller.isRunningDeviceAction
1790
- ? null
1791
- : () => controller.clickBrowserRuntime(
1792
- text: clickTextController.text.trim(),
1793
- ),
1794
- child: Text('Click Text'),
1795
- ),
1796
- ],
1797
- ),
1798
- const SizedBox(height: 18),
1799
- _DeviceFieldRow(
1800
- children: <Widget>[
1801
- _DeviceField(
1802
- label: 'Type Selector',
1803
- child: TextField(controller: fillSelectorController),
1804
- ),
1805
- _DeviceField(
1806
- label: 'Value',
1807
- child: TextField(controller: fillValueController),
1808
- ),
1809
- ],
1810
- ),
1811
- const SizedBox(height: 10),
1812
- OutlinedButton.icon(
1813
- onPressed: controller.isRunningDeviceAction
1814
- ? null
1815
- : () => controller.fillBrowserRuntime(
1816
- selector: fillSelectorController.text.trim(),
1817
- value: fillValueController.text,
1818
- ),
1819
- icon: Icon(Icons.keyboard_outlined),
1820
- label: Text('Type Into Field'),
1821
- ),
1822
- const SizedBox(height: 18),
1823
- _RuntimePreview(
1824
- title: 'Latest Browser Screenshot',
1825
- screenshotPath: controller.browserScreenshotPath,
1826
- controller: controller,
1827
- ),
1828
- if (controller.browserLastResult?.trim().isNotEmpty ??
1829
- false) ...<Widget>[
1830
- const SizedBox(height: 14),
1831
- _ResultBlock(
1832
- label: 'Last browser result',
1833
- value: controller.browserLastResult!,
1834
- ),
1835
- ],
1836
- ],
1837
- );
1838
- }
1839
- }
1840
-
1841
- // ignore: unused_element
1842
- class _AndroidControls extends StatelessWidget {
1843
- const _AndroidControls({
1844
- required this.controller,
1845
- required this.androidStatus,
1846
- required this.androidDevices,
1847
- required this.packageController,
1848
- required this.activityController,
1849
- required this.intentActionController,
1850
- required this.intentDataController,
1851
- required this.tapTextController,
1852
- required this.tapDescriptionController,
1853
- required this.tapResourceIdController,
1854
- required this.tapXController,
1855
- required this.tapYController,
1856
- required this.typeTextController,
1857
- required this.typeFieldTextController,
1858
- required this.typeFieldDescriptionController,
1859
- required this.waitTextController,
1860
- required this.keyController,
1861
- required this.swipeX1Controller,
1862
- required this.swipeY1Controller,
1863
- required this.swipeX2Controller,
1864
- required this.swipeY2Controller,
1865
- required this.toInt,
1866
- });
1867
-
1868
- final NeoAgentController controller;
1869
- final Map<String, dynamic> androidStatus;
1870
- final List<Map<String, dynamic>> androidDevices;
1871
- final TextEditingController packageController;
1872
- final TextEditingController activityController;
1873
- final TextEditingController intentActionController;
1874
- final TextEditingController intentDataController;
1875
- final TextEditingController tapTextController;
1876
- final TextEditingController tapDescriptionController;
1877
- final TextEditingController tapResourceIdController;
1878
- final TextEditingController tapXController;
1879
- final TextEditingController tapYController;
1880
- final TextEditingController typeTextController;
1881
- final TextEditingController typeFieldTextController;
1882
- final TextEditingController typeFieldDescriptionController;
1883
- final TextEditingController waitTextController;
1884
- final TextEditingController keyController;
1885
- final TextEditingController swipeX1Controller;
1886
- final TextEditingController swipeY1Controller;
1887
- final TextEditingController swipeX2Controller;
1888
- final TextEditingController swipeY2Controller;
1889
- final int? Function(String text) toInt;
1890
-
1891
- @override
1892
- Widget build(BuildContext context) {
1893
- return Column(
1894
- crossAxisAlignment: CrossAxisAlignment.start,
1895
- children: <Widget>[
1896
- Wrap(
1897
- spacing: 10,
1898
- runSpacing: 10,
1899
- children: <Widget>[
1900
- _MetaPill(
1901
- label: androidStatus['bootstrapped'] == true
1902
- ? 'SDK Ready'
1903
- : 'Bootstrap Needed',
1904
- icon: Icons.adb_outlined,
1905
- ),
1906
- _MetaPill(
1907
- label: androidStatus['serial']?.toString().isNotEmpty == true
1908
- ? androidStatus['serial'].toString()
1909
- : 'No active serial',
1910
- icon: Icons.phone_android_outlined,
1911
- ),
1912
- _MetaPill(
1913
- label: '${androidDevices.length} device(s)',
1914
- icon: Icons.devices_other_outlined,
1915
- ),
1916
- ],
1917
- ),
1918
- const SizedBox(height: 18),
1919
- Wrap(
1920
- spacing: 10,
1921
- runSpacing: 10,
1922
- children: <Widget>[
1923
- FilledButton.icon(
1924
- onPressed: controller.isRunningDeviceAction
1925
- ? null
1926
- : controller.startAndroidRuntime,
1927
- icon: Icon(Icons.play_arrow_outlined),
1928
- label: Text('Start Emulator'),
1929
- ),
1930
- OutlinedButton.icon(
1931
- onPressed: controller.isRunningDeviceAction
1932
- ? null
1933
- : controller.stopAndroidRuntime,
1934
- icon: Icon(Icons.stop_circle_outlined),
1935
- label: Text('Stop'),
1936
- ),
1937
- OutlinedButton.icon(
1938
- onPressed: controller.isRunningDeviceAction
1939
- ? null
1940
- : controller.screenshotAndroidRuntime,
1941
- icon: Icon(Icons.photo_camera_outlined),
1942
- label: Text('Screenshot'),
1943
- ),
1944
- OutlinedButton.icon(
1945
- onPressed: controller.isRunningDeviceAction
1946
- ? null
1947
- : controller.dumpAndroidUiRuntime,
1948
- icon: Icon(Icons.data_object_outlined),
1949
- label: Text('Dump UI'),
1950
- ),
1951
- OutlinedButton.icon(
1952
- onPressed: controller.isRunningDeviceAction
1953
- ? null
1954
- : controller.refreshAndroidApps,
1955
- icon: Icon(Icons.apps_outlined),
1956
- label: Text('Load Apps'),
1957
- ),
1958
- ],
1959
- ),
1960
- const SizedBox(height: 18),
1961
- _DeviceFieldRow(
1962
- children: <Widget>[
1963
- _DeviceField(
1964
- label: 'Package',
1965
- child: TextField(controller: packageController),
1966
- ),
1967
- _DeviceField(
1968
- label: 'Activity',
1969
- child: TextField(controller: activityController),
1970
- ),
1971
- ],
1972
- ),
1973
- const SizedBox(height: 10),
1974
- Wrap(
1975
- spacing: 10,
1976
- runSpacing: 10,
1977
- children: <Widget>[
1978
- OutlinedButton.icon(
1979
- onPressed: controller.isRunningDeviceAction
1980
- ? null
1981
- : () => controller.openAndroidAppRuntime(
1982
- packageName: packageController.text.trim(),
1983
- activity: activityController.text.trim(),
1984
- ),
1985
- icon: Icon(Icons.apps),
1986
- label: Text('Open App'),
1987
- ),
1988
- if (controller.androidInstalledApps.isNotEmpty)
1989
- SizedBox(width: 1, height: 1, child: Container()),
1990
- ],
1991
- ),
1992
- if (controller.androidInstalledApps.isNotEmpty) ...<Widget>[
1993
- const SizedBox(height: 8),
1994
- Wrap(
1995
- spacing: 8,
1996
- runSpacing: 8,
1997
- children: controller.androidInstalledApps.take(10).map((appId) {
1998
- return ActionChip(
1999
- label: Text(appId),
2000
- onPressed: () => packageController.text = appId,
2001
- );
2002
- }).toList(),
2003
- ),
2004
- ],
2005
- const SizedBox(height: 18),
2006
- _DeviceFieldRow(
2007
- children: <Widget>[
2008
- _DeviceField(
2009
- label: 'Intent Action',
2010
- child: TextField(controller: intentActionController),
2011
- ),
2012
- _DeviceField(
2013
- label: 'Intent Data',
2014
- child: TextField(controller: intentDataController),
2015
- ),
2016
- ],
2017
- ),
2018
- const SizedBox(height: 10),
2019
- OutlinedButton.icon(
2020
- onPressed: controller.isRunningDeviceAction
2021
- ? null
2022
- : () => controller.openAndroidIntentRuntime(
2023
- action: intentActionController.text.trim(),
2024
- dataUri: intentDataController.text.trim(),
2025
- packageName: packageController.text.trim(),
2026
- ),
2027
- icon: Icon(Icons.route_outlined),
2028
- label: Text('Open Intent'),
2029
- ),
2030
- const SizedBox(height: 18),
2031
- _DeviceFieldRow(
2032
- children: <Widget>[
2033
- _DeviceField(
2034
- label: 'Wait For Text',
2035
- child: TextField(controller: waitTextController),
2036
- ),
2037
- _DeviceField(
2038
- label: 'Key',
2039
- child: TextField(controller: keyController),
2040
- ),
2041
- ],
2042
- ),
2043
- const SizedBox(height: 10),
2044
- Wrap(
2045
- spacing: 10,
2046
- runSpacing: 10,
2047
- children: <Widget>[
2048
- OutlinedButton(
2049
- onPressed: controller.isRunningDeviceAction
2050
- ? null
2051
- : () => controller.waitForAndroidRuntime(<String, dynamic>{
2052
- 'text': waitTextController.text.trim(),
2053
- 'timeoutMs': 20000,
2054
- 'intervalMs': 1200,
2055
- }),
2056
- child: Text('Wait For UI'),
2057
- ),
2058
- OutlinedButton(
2059
- onPressed: controller.isRunningDeviceAction
2060
- ? null
2061
- : () => controller.pressAndroidKeyRuntime(
2062
- keyController.text.trim(),
2063
- ),
2064
- child: Text('Press Key'),
2065
- ),
2066
- ],
2067
- ),
2068
- const SizedBox(height: 18),
2069
- _DeviceFieldRow(
2070
- children: <Widget>[
2071
- _DeviceField(
2072
- label: 'Tap Text',
2073
- child: TextField(controller: tapTextController),
2074
- ),
2075
- _DeviceField(
2076
- label: 'Tap Description',
2077
- child: TextField(controller: tapDescriptionController),
2078
- ),
2079
- ],
2080
- ),
2081
- const SizedBox(height: 10),
2082
- _DeviceFieldRow(
2083
- children: <Widget>[
2084
- _DeviceField(
2085
- label: 'Tap Resource Id',
2086
- child: TextField(controller: tapResourceIdController),
2087
- ),
2088
- _DeviceField(
2089
- label: 'Tap X / Y',
2090
- child: Row(
2091
- children: <Widget>[
2092
- Expanded(child: TextField(controller: tapXController)),
2093
- const SizedBox(width: 8),
2094
- Expanded(child: TextField(controller: tapYController)),
2095
- ],
2096
- ),
2097
- ),
2098
- ],
2099
- ),
2100
- const SizedBox(height: 10),
2101
- OutlinedButton.icon(
2102
- onPressed: controller.isRunningDeviceAction
2103
- ? null
2104
- : () => controller.tapAndroidRuntime(<String, dynamic>{
2105
- if (tapTextController.text.trim().isNotEmpty)
2106
- 'text': tapTextController.text.trim(),
2107
- if (tapDescriptionController.text.trim().isNotEmpty)
2108
- 'description': tapDescriptionController.text.trim(),
2109
- if (tapResourceIdController.text.trim().isNotEmpty)
2110
- 'resourceId': tapResourceIdController.text.trim(),
2111
- if (toInt(tapXController.text) != null)
2112
- 'x': toInt(tapXController.text),
2113
- if (toInt(tapYController.text) != null)
2114
- 'y': toInt(tapYController.text),
2115
- }),
2116
- icon: Icon(Icons.touch_app_outlined),
2117
- label: Text('Tap'),
2118
- ),
2119
- const SizedBox(height: 18),
2120
- _DeviceFieldRow(
2121
- children: <Widget>[
2122
- _DeviceField(
2123
- label: 'Type Text',
2124
- child: TextField(controller: typeTextController),
2125
- ),
2126
- _DeviceField(
2127
- label: 'Focus Field Text / Description',
2128
- child: Row(
2129
- children: <Widget>[
2130
- Expanded(
2131
- child: TextField(controller: typeFieldTextController),
2132
- ),
2133
- const SizedBox(width: 8),
2134
- Expanded(
2135
- child: TextField(
2136
- controller: typeFieldDescriptionController,
2137
- ),
2138
- ),
2139
- ],
2140
- ),
2141
- ),
2142
- ],
2143
- ),
2144
- const SizedBox(height: 10),
2145
- OutlinedButton.icon(
2146
- onPressed: controller.isRunningDeviceAction
2147
- ? null
2148
- : () => controller.typeAndroidRuntime(<String, dynamic>{
2149
- 'text': typeTextController.text,
2150
- if (typeFieldTextController.text.trim().isNotEmpty)
2151
- 'textSelector': typeFieldTextController.text.trim(),
2152
- if (typeFieldDescriptionController.text.trim().isNotEmpty)
2153
- 'description': typeFieldDescriptionController.text.trim(),
2154
- 'pressEnter': true,
2155
- }),
2156
- icon: Icon(Icons.keyboard_outlined),
2157
- label: Text('Type'),
2158
- ),
2159
- const SizedBox(height: 18),
2160
- _DeviceFieldRow(
2161
- children: <Widget>[
2162
- _DeviceField(
2163
- label: 'Swipe X1 / Y1',
2164
- child: Row(
2165
- children: <Widget>[
2166
- Expanded(child: TextField(controller: swipeX1Controller)),
2167
- const SizedBox(width: 8),
2168
- Expanded(child: TextField(controller: swipeY1Controller)),
2169
- ],
2170
- ),
2171
- ),
2172
- _DeviceField(
2173
- label: 'Swipe X2 / Y2',
2174
- child: Row(
2175
- children: <Widget>[
2176
- Expanded(child: TextField(controller: swipeX2Controller)),
2177
- const SizedBox(width: 8),
2178
- Expanded(child: TextField(controller: swipeY2Controller)),
2179
- ],
2180
- ),
2181
- ),
2182
- ],
2183
- ),
2184
- const SizedBox(height: 10),
2185
- OutlinedButton.icon(
2186
- onPressed: controller.isRunningDeviceAction
2187
- ? null
2188
- : () => controller.swipeAndroidRuntime(<String, dynamic>{
2189
- 'x1': toInt(swipeX1Controller.text),
2190
- 'y1': toInt(swipeY1Controller.text),
2191
- 'x2': toInt(swipeX2Controller.text),
2192
- 'y2': toInt(swipeY2Controller.text),
2193
- }),
2194
- icon: Icon(Icons.swipe_outlined),
2195
- label: Text('Swipe'),
2196
- ),
2197
- const SizedBox(height: 18),
2198
- _RuntimePreview(
2199
- title: 'Latest Android Screenshot',
2200
- screenshotPath: controller.androidScreenshotPath,
2201
- controller: controller,
2202
- ),
2203
- if (controller.androidUiPreview.isNotEmpty) ...<Widget>[
2204
- const SizedBox(height: 14),
2205
- Text(
2206
- 'Latest UI dump preview',
2207
- style: TextStyle(fontWeight: FontWeight.w700),
2208
- ),
2209
- const SizedBox(height: 10),
2210
- ...controller.androidUiPreview.take(6).map((node) {
2211
- final title = node['text']?.toString().trim().isNotEmpty == true
2212
- ? node['text'].toString()
2213
- : node['description']?.toString().trim().isNotEmpty == true
2214
- ? node['description'].toString()
2215
- : node['resourceId']?.toString().trim().isNotEmpty == true
2216
- ? node['resourceId'].toString()
2217
- : node['className']?.toString() ?? 'node';
2218
- return Container(
2219
- margin: const EdgeInsets.only(bottom: 8),
2220
- padding: const EdgeInsets.all(12),
2221
- decoration: BoxDecoration(
2222
- color: _bgSecondary,
2223
- borderRadius: BorderRadius.circular(10),
2224
- border: Border.all(color: _border),
2225
- ),
2226
- child: Text(
2227
- '$title\n${node['packageName'] ?? ''}',
2228
- style: TextStyle(color: _textSecondary, height: 1.5),
2229
- ),
2230
- );
2231
- }),
2232
- ],
2233
- if (controller.androidLastResult?.trim().isNotEmpty ??
2234
- false) ...<Widget>[
2235
- const SizedBox(height: 14),
2236
- _ResultBlock(
2237
- label: 'Last Android result',
2238
- value: controller.androidLastResult!,
2239
- ),
2240
- ],
2241
- ],
2242
- );
2243
- }
2244
- }
2245
1580
 
2246
1581
  class _DeviceFieldRow extends StatelessWidget {
2247
1582
  const _DeviceFieldRow({required this.children});