neoagent 2.4.1-beta.8 → 2.4.1
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.
- package/.env.example +33 -3
- package/LICENSE +111 -56
- package/README.md +8 -3
- package/docs/configuration.md +8 -0
- package/docs/getting-started.md +9 -3
- package/docs/index.md +4 -0
- package/extensions/chrome-browser/background.mjs +45 -7
- package/extensions/chrome-browser/icons/icon128.png +0 -0
- package/extensions/chrome-browser/icons/icon16.png +0 -0
- package/extensions/chrome-browser/icons/icon48.png +0 -0
- package/extensions/chrome-browser/icons/logo.svg +39 -8
- package/extensions/chrome-browser/manifest.json +3 -2
- package/extensions/chrome-browser/popup.html +5 -1
- package/extensions/chrome-browser/popup.js +15 -2
- package/flutter_app/android/app/src/main/AndroidManifest.xml +2 -1
- package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/flutter_app/assets/branding/app_icon_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_64.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_64.png +0 -0
- package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
- package/flutter_app/assets/branding/tray_icon_light_template.png +0 -0
- package/flutter_app/assets/branding/tray_icon_template.png +0 -0
- package/flutter_app/lib/features/location/location_service.dart +3 -0
- package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +391 -382
- package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +743 -0
- package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +18 -16
- package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +19 -18
- package/flutter_app/lib/features/onboarding/onboarding_shell.dart +8 -1
- package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +16 -13
- package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +17 -13
- package/flutter_app/lib/main.dart +3 -0
- package/flutter_app/lib/main_account_settings.dart +10 -34
- package/flutter_app/lib/main_admin.dart +14 -1
- package/flutter_app/lib/main_app_shell.dart +377 -340
- package/flutter_app/lib/main_chat.dart +707 -227
- package/flutter_app/lib/main_controller.dart +338 -46
- package/flutter_app/lib/main_devices.dart +851 -122
- package/flutter_app/lib/main_integrations.dart +255 -6
- package/flutter_app/lib/main_launcher.dart +1 -1
- package/flutter_app/lib/main_model_picker.dart +685 -0
- package/flutter_app/lib/main_models.dart +212 -0
- package/flutter_app/lib/main_navigation.dart +1 -9
- package/flutter_app/lib/main_operations.dart +1417 -614
- package/flutter_app/lib/main_settings.dart +764 -887
- package/flutter_app/lib/main_shared.dart +971 -443
- package/flutter_app/lib/main_spacing.dart +4 -4
- package/flutter_app/lib/main_theme.dart +22 -14
- package/flutter_app/lib/main_unified.dart +5 -72
- package/flutter_app/lib/src/android_apk_drop_zone.dart +24 -0
- package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +13 -0
- package/flutter_app/lib/src/android_apk_drop_zone_web.dart +219 -0
- package/flutter_app/lib/src/backend_client.dart +79 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +56 -7
- package/flutter_app/lib/src/desktop_companion_io.dart +77 -1
- package/flutter_app/lib/src/desktop_native_bridge.dart +13 -0
- package/flutter_app/lib/src/security/password_strength.dart +84 -0
- package/flutter_app/lib/src/stream_renderer.dart +205 -35
- package/flutter_app/lib/src/theme/palette.dart +76 -34
- package/flutter_app/linux/runner/resources/app_icon.png +0 -0
- package/flutter_app/macos/Runner/AppDelegate.swift +44 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
- package/flutter_app/pubspec.lock +2 -2
- package/flutter_app/pubspec.yaml +7 -1
- package/flutter_app/tool/branding_source/neoagent-icon-1024.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-128.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-16.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-180.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-192.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-256.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-32.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-48.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-512.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-64.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon.svg +43 -0
- package/flutter_app/tool/generate_desktop_branding.py +154 -152
- package/flutter_app/web/favicon.png +0 -0
- package/flutter_app/web/favicon.svg +40 -9
- package/flutter_app/web/favicon_light.svg +43 -0
- package/flutter_app/web/icons/Icon-192-light.png +0 -0
- package/flutter_app/web/icons/Icon-192.png +0 -0
- package/flutter_app/web/icons/Icon-512-light.png +0 -0
- package/flutter_app/web/icons/Icon-512.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192-light.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512-light.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
- package/flutter_app/windows/runner/main.cpp +7 -1
- package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
- package/lib/manager.js +445 -83
- package/package.json +17 -3
- package/runtime/paths.js +3 -1
- package/server/admin/access.js +198 -0
- package/server/admin/admin.css +268 -0
- package/server/admin/admin.js +348 -0
- package/server/admin/analytics.js +128 -0
- package/server/admin/index.html +1015 -0
- package/server/admin/login.html +290 -0
- package/server/admin/logo.svg +43 -0
- package/server/admin/sql.js +134 -0
- package/server/admin/users.js +147 -0
- package/server/config/origins.js +3 -1
- package/server/db/database.js +92 -0
- package/server/db/ftsQuery.js +27 -0
- package/server/http/routes.js +1 -0
- package/server/http/static.js +23 -6
- package/server/index.js +1 -1
- package/server/middleware/adminAuth.js +48 -0
- package/server/middleware/auth.js +1 -40
- package/server/public/.last_build_id +1 -1
- package/server/public/app_icon.png +0 -0
- package/server/public/assets/AssetManifest.bin +1 -1
- package/server/public/assets/AssetManifest.bin.json +1 -1
- package/server/public/assets/assets/branding/app_icon_1024.png +0 -0
- package/server/public/assets/assets/branding/app_icon_256.png +0 -0
- package/server/public/assets/assets/branding/app_icon_512.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_1024.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_256.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_512.png +0 -0
- package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
- package/server/public/assets/assets/branding/tray_icon_light_template.png +0 -0
- package/server/public/assets/assets/branding/tray_icon_template.png +0 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/assets/web/icons/Icon-192.png +0 -0
- package/server/public/favicon.png +0 -0
- package/server/public/favicon.svg +40 -9
- package/server/public/favicon_light.svg +43 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/icons/Icon-192-light.png +0 -0
- package/server/public/icons/Icon-192.png +0 -0
- package/server/public/icons/Icon-512-light.png +0 -0
- package/server/public/icons/Icon-512.png +0 -0
- package/server/public/icons/Icon-maskable-192-light.png +0 -0
- package/server/public/icons/Icon-maskable-192.png +0 -0
- package/server/public/icons/Icon-maskable-512-light.png +0 -0
- package/server/public/icons/Icon-maskable-512.png +0 -0
- package/server/public/main.dart.js +88944 -85720
- package/server/routes/admin.js +632 -0
- package/server/routes/agent_profiles.js +3 -0
- package/server/routes/agents.js +1 -1
- package/server/routes/auth.js +3 -1
- package/server/routes/browser.js +14 -0
- package/server/routes/browser_extension.js +21 -4
- package/server/routes/desktop.js +10 -0
- package/server/routes/mcp.js +29 -13
- package/server/routes/memory.js +23 -1
- package/server/routes/messaging.js +2 -0
- package/server/routes/screenHistory.js +14 -8
- package/server/routes/settings.js +14 -2
- package/server/routes/stream.js +12 -3
- package/server/routes/triggers.js +4 -4
- package/server/routes/voice_assistant.js +36 -1
- package/server/routes/workspace.js +86 -0
- package/server/services/account/admin_two_factor.js +132 -0
- package/server/services/account/password_policy.js +6 -1
- package/server/services/ai/completion.js +44 -0
- package/server/services/ai/engine.js +321 -500
- package/server/services/ai/imageAnalysis.js +9 -5
- package/server/services/ai/logFormat.js +46 -0
- package/server/services/ai/loopPolicy.js +11 -0
- package/server/services/ai/messagingFallback.js +228 -0
- package/server/services/ai/models.js +194 -55
- package/server/services/ai/providerRetry.js +169 -0
- package/server/services/ai/providers/anthropic.js +15 -4
- package/server/services/ai/providers/google.js +38 -11
- package/server/services/ai/providers/grok.js +19 -68
- package/server/services/ai/providers/grokOauth.js +141 -0
- package/server/services/ai/providers/nvidia.js +154 -0
- package/server/services/ai/providers/ollama.js +76 -39
- package/server/services/ai/providers/openai.js +20 -31
- package/server/services/ai/providers/openaiCodex.js +6 -2
- package/server/services/ai/providers/openaiCompatible.js +70 -0
- package/server/services/ai/providers/openrouter.js +162 -0
- package/server/services/ai/settings.js +30 -0
- package/server/services/ai/systemPrompt.js +51 -29
- package/server/services/ai/taskAnalysis.js +60 -1
- package/server/services/ai/toolEvidence.js +207 -0
- package/server/services/ai/toolSelector.js +8 -1
- package/server/services/ai/tools.js +80 -15
- package/server/services/android/controller.js +36 -1
- package/server/services/browser/controller.js +18 -0
- package/server/services/browser/extension/manifest.js +33 -0
- package/server/services/browser/extension/provider.js +12 -6
- package/server/services/browser/extension/registry.js +229 -18
- package/server/services/desktop/protocol.js +1 -0
- package/server/services/desktop/provider.js +4 -0
- package/server/services/desktop/registry.js +104 -1
- package/server/services/desktop/screenRecorder.js +208 -98
- package/server/services/desktop/screen_recorder_support.js +46 -0
- package/server/services/integrations/google/provider.js +13 -0
- package/server/services/integrations/home_assistant/constants.js +88 -0
- package/server/services/integrations/home_assistant/network.js +207 -0
- package/server/services/integrations/home_assistant/provider.js +249 -0
- package/server/services/integrations/home_assistant/snapshot.js +98 -0
- package/server/services/integrations/home_assistant/tools.js +101 -0
- package/server/services/integrations/manager.js +5 -0
- package/server/services/integrations/registry.js +2 -0
- package/server/services/integrations/trello/provider.js +8 -2
- package/server/services/manager.js +51 -47
- package/server/services/mcp/client.js +208 -268
- package/server/services/mcp/client_support.js +172 -0
- package/server/services/mcp/recovery.js +116 -0
- package/server/services/mcp/tool_operations.js +123 -0
- package/server/services/memory/ingestion.js +185 -370
- package/server/services/memory/ingestion_coverage.js +129 -0
- package/server/services/memory/ingestion_documents.js +123 -0
- package/server/services/memory/ingestion_support.js +171 -0
- package/server/services/memory/intelligence.js +181 -0
- package/server/services/memory/manager.js +476 -32
- package/server/services/messaging/automation.js +71 -134
- package/server/services/messaging/formatting_guides.js +26 -1
- package/server/services/messaging/inbound_queue.js +111 -0
- package/server/services/messaging/manager.js +25 -8
- package/server/services/messaging/typing_keepalive.js +110 -0
- package/server/services/runtime/manager.js +7 -3
- package/server/services/runtime/settings.js +17 -0
- package/server/services/streaming/android-stream.js +296 -32
- package/server/services/streaming/stream-hub.js +1 -1
- package/server/services/tasks/integration_runtime.js +4 -1
- package/server/services/tasks/runtime.js +415 -56
- package/server/services/tasks/task_repository.js +48 -6
- package/server/services/websocket.js +8 -2
- package/server/services/widgets/service.js +17 -1
- package/server/services/workspace/manager.js +116 -0
- package/server/utils/logger.js +44 -6
- package/server/utils/security.js +3 -0
- package/server/utils/version.js +29 -19
- package/server/services/memory/openhuman_uplift.test.js +0 -98
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
part of 'main.dart';
|
|
2
2
|
|
|
3
|
+
// ignore_for_file: unused_element
|
|
4
|
+
|
|
3
5
|
class _OptimizedScreenshotPayload {
|
|
4
6
|
const _OptimizedScreenshotPayload({
|
|
5
7
|
required this.bytes,
|
|
@@ -26,6 +28,7 @@ class _DevicesPanelState extends State<DevicesPanel> {
|
|
|
26
28
|
late final TextEditingController _androidLaunchController;
|
|
27
29
|
late final TextEditingController _desktopLaunchController;
|
|
28
30
|
late final TextEditingController _textEntryController;
|
|
31
|
+
late final TextEditingController _workspaceEditorController;
|
|
29
32
|
Timer? _surfaceFrameTimer;
|
|
30
33
|
_DeviceSurface _surface = _DeviceSurface.browser;
|
|
31
34
|
_DeviceSurface? _runningSurface;
|
|
@@ -39,6 +42,7 @@ class _DevicesPanelState extends State<DevicesPanel> {
|
|
|
39
42
|
);
|
|
40
43
|
_desktopLaunchController = TextEditingController();
|
|
41
44
|
_textEntryController = TextEditingController();
|
|
45
|
+
_workspaceEditorController = TextEditingController();
|
|
42
46
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
43
47
|
if (!mounted) {
|
|
44
48
|
return;
|
|
@@ -57,6 +61,7 @@ class _DevicesPanelState extends State<DevicesPanel> {
|
|
|
57
61
|
_androidLaunchController,
|
|
58
62
|
_desktopLaunchController,
|
|
59
63
|
_textEntryController,
|
|
64
|
+
_workspaceEditorController,
|
|
60
65
|
]) {
|
|
61
66
|
controller.dispose();
|
|
62
67
|
}
|
|
@@ -66,6 +71,7 @@ class _DevicesPanelState extends State<DevicesPanel> {
|
|
|
66
71
|
|
|
67
72
|
bool get _isBrowser => _surface == _DeviceSurface.browser;
|
|
68
73
|
bool get _isDesktop => _surface == _DeviceSurface.desktop;
|
|
74
|
+
bool get _isFiles => _surface == _DeviceSurface.files;
|
|
69
75
|
|
|
70
76
|
bool get _isCurrentSurfaceBusy =>
|
|
71
77
|
widget.controller.isRunningDeviceAction &&
|
|
@@ -90,6 +96,29 @@ class _DevicesPanelState extends State<DevicesPanel> {
|
|
|
90
96
|
bool get _androidStarting =>
|
|
91
97
|
widget.controller.androidRuntime['starting'] == true;
|
|
92
98
|
|
|
99
|
+
String? get _androidDeviceId {
|
|
100
|
+
final status = widget.controller.androidRuntime;
|
|
101
|
+
final direct = status['adbSerial']?.toString().trim();
|
|
102
|
+
if (direct != null && direct.isNotEmpty) {
|
|
103
|
+
return direct;
|
|
104
|
+
}
|
|
105
|
+
final devices = _jsonMapList(status['devices'], fallbackToMapValues: true);
|
|
106
|
+
for (final device in devices) {
|
|
107
|
+
if (device['status']?.toString() != 'device') {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
final serial = device['serial']?.toString().trim();
|
|
111
|
+
if (serial != null && serial.isNotEmpty) {
|
|
112
|
+
return serial;
|
|
113
|
+
}
|
|
114
|
+
final id = device['deviceId']?.toString().trim();
|
|
115
|
+
if (id != null && id.isNotEmpty) {
|
|
116
|
+
return id;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
93
122
|
List<Map<String, dynamic>> get _onlineDesktopDevices => widget
|
|
94
123
|
.controller
|
|
95
124
|
.desktopDevices
|
|
@@ -107,6 +136,9 @@ class _DevicesPanelState extends State<DevicesPanel> {
|
|
|
107
136
|
widget.controller.browserBackend == 'extension' &&
|
|
108
137
|
!widget.controller.browserExtensionConnected;
|
|
109
138
|
|
|
139
|
+
List<Map<String, dynamic>> get _browserExtensionDevices =>
|
|
140
|
+
widget.controller.browserExtensionTokens;
|
|
141
|
+
|
|
110
142
|
String? get _activeScreenshotPath {
|
|
111
143
|
if (_isBrowser) {
|
|
112
144
|
if (_extensionPreferredButOffline) return null;
|
|
@@ -154,6 +186,13 @@ class _DevicesPanelState extends State<DevicesPanel> {
|
|
|
154
186
|
return;
|
|
155
187
|
}
|
|
156
188
|
|
|
189
|
+
if (_isFiles) {
|
|
190
|
+
if (controller.workspaceEntries.isEmpty) {
|
|
191
|
+
await controller.refreshWorkspaceFiles();
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
157
196
|
if (_androidOnline && (controller.androidScreenshotPath ?? '').isEmpty) {
|
|
158
197
|
await controller.screenshotAndroidRuntime();
|
|
159
198
|
}
|
|
@@ -175,6 +214,9 @@ class _DevicesPanelState extends State<DevicesPanel> {
|
|
|
175
214
|
await widget.controller.refreshDesktopFrameRuntime();
|
|
176
215
|
return;
|
|
177
216
|
}
|
|
217
|
+
if (_isFiles) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
178
220
|
if (_androidStarting) {
|
|
179
221
|
await widget.controller.refreshDevices();
|
|
180
222
|
if (_androidOnline &&
|
|
@@ -232,6 +274,11 @@ class _DevicesPanelState extends State<DevicesPanel> {
|
|
|
232
274
|
return;
|
|
233
275
|
}
|
|
234
276
|
|
|
277
|
+
if (_isFiles) {
|
|
278
|
+
await controller.refreshWorkspaceFiles();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
235
282
|
if (!_androidOnline) {
|
|
236
283
|
await controller.startAndroidRuntime();
|
|
237
284
|
await widget.controller.refreshDevices();
|
|
@@ -273,6 +320,9 @@ class _DevicesPanelState extends State<DevicesPanel> {
|
|
|
273
320
|
}
|
|
274
321
|
return;
|
|
275
322
|
}
|
|
323
|
+
if (_isFiles) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
276
326
|
if (!_androidOnline) {
|
|
277
327
|
return;
|
|
278
328
|
}
|
|
@@ -288,6 +338,8 @@ class _DevicesPanelState extends State<DevicesPanel> {
|
|
|
288
338
|
await widget.controller.typeBrowserTextRuntime(text, pressEnter: true);
|
|
289
339
|
} else if (_isDesktop) {
|
|
290
340
|
await widget.controller.typeDesktopRuntime(text, pressEnter: true);
|
|
341
|
+
} else if (_isFiles) {
|
|
342
|
+
return;
|
|
291
343
|
} else {
|
|
292
344
|
await widget.controller.typeAndroidRuntime(<String, dynamic>{
|
|
293
345
|
'text': text,
|
|
@@ -296,6 +348,23 @@ class _DevicesPanelState extends State<DevicesPanel> {
|
|
|
296
348
|
}
|
|
297
349
|
});
|
|
298
350
|
|
|
351
|
+
void _handleHover(Offset point) {
|
|
352
|
+
if (_isBrowser) {
|
|
353
|
+
unawaited(widget.controller.hoverBrowserPointRuntime(
|
|
354
|
+
x: point.dx.round(),
|
|
355
|
+
y: point.dy.round(),
|
|
356
|
+
));
|
|
357
|
+
} else if (_isDesktop) {
|
|
358
|
+
if (_desktopRequiresSelection) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
unawaited(widget.controller.hoverDesktopRuntime(
|
|
362
|
+
x: point.dx.round(),
|
|
363
|
+
y: point.dy.round(),
|
|
364
|
+
));
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
299
368
|
Future<void> _handleTap(Offset point) => _runOnSurface(() async {
|
|
300
369
|
if (_isBrowser) {
|
|
301
370
|
await widget.controller.clickBrowserPointRuntime(
|
|
@@ -324,36 +393,37 @@ class _DevicesPanelState extends State<DevicesPanel> {
|
|
|
324
393
|
});
|
|
325
394
|
});
|
|
326
395
|
|
|
327
|
-
Future<void> _handleSwipe(Offset start, Offset end) =>
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
396
|
+
Future<void> _handleSwipe(Offset start, Offset end) =>
|
|
397
|
+
_runOnSurface(() async {
|
|
398
|
+
if (_isBrowser) {
|
|
399
|
+
await widget.controller.scrollBrowserRuntime(
|
|
400
|
+
deltaY: (start.dy - end.dy).round(),
|
|
401
|
+
);
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (_isDesktop) {
|
|
405
|
+
if (_desktopRequiresSelection) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
await widget.controller.dragDesktopRuntime(
|
|
409
|
+
x1: start.dx.round(),
|
|
410
|
+
y1: start.dy.round(),
|
|
411
|
+
x2: end.dx.round(),
|
|
412
|
+
y2: end.dy.round(),
|
|
413
|
+
);
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
if (!_androidOnline) {
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
await widget.controller.swipeAndroidRuntime(<String, dynamic>{
|
|
420
|
+
'x1': start.dx.round(),
|
|
421
|
+
'y1': start.dy.round(),
|
|
422
|
+
'x2': end.dx.round(),
|
|
423
|
+
'y2': end.dy.round(),
|
|
424
|
+
'durationMs': 280,
|
|
425
|
+
});
|
|
426
|
+
});
|
|
357
427
|
|
|
358
428
|
Future<void> _runQuickAction(String action) => _runOnSurface(() async {
|
|
359
429
|
final controller = widget.controller;
|
|
@@ -398,6 +468,13 @@ class _DevicesPanelState extends State<DevicesPanel> {
|
|
|
398
468
|
final prefersExtension = controller.browserBackend == 'extension';
|
|
399
469
|
final extensionConnected = controller.browserExtensionConnected;
|
|
400
470
|
final usingExtension = prefersExtension && extensionConnected;
|
|
471
|
+
final selectedBrowserExtension = controller.browserExtensionTokens
|
|
472
|
+
.where(
|
|
473
|
+
(device) =>
|
|
474
|
+
device['tokenId'] == controller.selectedBrowserExtensionTokenId,
|
|
475
|
+
)
|
|
476
|
+
.cast<Map<String, dynamic>?>()
|
|
477
|
+
.firstWhere((device) => device != null, orElse: () => null);
|
|
401
478
|
final browserFallbackLabel = 'cloud browser runtime';
|
|
402
479
|
final browserPageInfo = browserStatus['pageInfo'] is Map<dynamic, dynamic>
|
|
403
480
|
? Map<String, dynamic>.from(browserStatus['pageInfo'] as Map)
|
|
@@ -454,12 +531,75 @@ class _DevicesPanelState extends State<DevicesPanel> {
|
|
|
454
531
|
desktopDevices: controller.desktopDevices,
|
|
455
532
|
selectedDesktopDeviceId:
|
|
456
533
|
controller.selectedDesktopDeviceId,
|
|
534
|
+
browserExtensionDevices:
|
|
535
|
+
controller.browserExtensionTokens,
|
|
536
|
+
selectedBrowserExtensionTokenId:
|
|
537
|
+
controller.selectedBrowserExtensionTokenId,
|
|
457
538
|
browserExtensionPreferred: prefersExtension,
|
|
458
539
|
browserExtensionActive: usingExtension,
|
|
459
540
|
browserFallbackLabel: browserFallbackLabel,
|
|
460
541
|
),
|
|
542
|
+
if (_isFiles) ...<Widget>[
|
|
543
|
+
const SizedBox(height: 14),
|
|
544
|
+
_WorkspaceExplorer(
|
|
545
|
+
controller: controller,
|
|
546
|
+
editorController: _workspaceEditorController,
|
|
547
|
+
),
|
|
548
|
+
const SizedBox(height: 14),
|
|
549
|
+
_SurfaceSwitcher(
|
|
550
|
+
surface: _surface,
|
|
551
|
+
onSelect: _selectSurface,
|
|
552
|
+
),
|
|
553
|
+
] else ...<Widget>[
|
|
461
554
|
if (_isBrowser && prefersExtension) ...<Widget>[
|
|
462
555
|
const SizedBox(height: 14),
|
|
556
|
+
if (_browserExtensionDevices.isNotEmpty) ...<Widget>[
|
|
557
|
+
DropdownButtonFormField<String>(
|
|
558
|
+
isExpanded: true,
|
|
559
|
+
initialValue: selectedBrowserExtension?['tokenId']
|
|
560
|
+
?.toString(),
|
|
561
|
+
decoration: const InputDecoration(
|
|
562
|
+
labelText: 'Chrome extension device',
|
|
563
|
+
prefixIcon: Icon(Icons.extension_outlined),
|
|
564
|
+
),
|
|
565
|
+
hint: const Text('Select a paired extension'),
|
|
566
|
+
items: _browserExtensionDevices.map((device) {
|
|
567
|
+
final tokenId = device['tokenId']?.toString() ?? '';
|
|
568
|
+
final label =
|
|
569
|
+
device['name']?.toString().trim().isNotEmpty ==
|
|
570
|
+
true
|
|
571
|
+
? device['name'].toString()
|
|
572
|
+
: tokenId;
|
|
573
|
+
final state =
|
|
574
|
+
device['online'] == true ||
|
|
575
|
+
device['connected'] == true
|
|
576
|
+
? 'online'
|
|
577
|
+
: 'offline';
|
|
578
|
+
return DropdownMenuItem<String>(
|
|
579
|
+
value: tokenId,
|
|
580
|
+
child: Text(
|
|
581
|
+
'$label · $state',
|
|
582
|
+
maxLines: 1,
|
|
583
|
+
overflow: TextOverflow.ellipsis,
|
|
584
|
+
softWrap: false,
|
|
585
|
+
),
|
|
586
|
+
);
|
|
587
|
+
}).toList(),
|
|
588
|
+
onChanged: _isCurrentSurfaceBusy
|
|
589
|
+
? null
|
|
590
|
+
: (value) {
|
|
591
|
+
if (value == null || value.isEmpty) {
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
unawaited(
|
|
595
|
+
controller.selectBrowserExtensionRuntime(
|
|
596
|
+
value,
|
|
597
|
+
),
|
|
598
|
+
);
|
|
599
|
+
},
|
|
600
|
+
),
|
|
601
|
+
const SizedBox(height: 10),
|
|
602
|
+
],
|
|
463
603
|
_ExtensionStatusBar(
|
|
464
604
|
connected: extensionConnected,
|
|
465
605
|
onDownload: controller.downloadBrowserExtension,
|
|
@@ -570,6 +710,21 @@ class _DevicesPanelState extends State<DevicesPanel> {
|
|
|
570
710
|
surface: _surface,
|
|
571
711
|
controller: controller,
|
|
572
712
|
screenshotPath: _activeScreenshotPath,
|
|
713
|
+
streamPlatform: _isBrowser && browserStatus['launched'] == true
|
|
714
|
+
? 'browser'
|
|
715
|
+
: (_isDesktop && _desktopOnline
|
|
716
|
+
? 'desktop'
|
|
717
|
+
: (!_isBrowser && !_isDesktop && _androidOnline
|
|
718
|
+
? 'android'
|
|
719
|
+
: null)),
|
|
720
|
+
streamDeviceId: _isBrowser && browserStatus['launched'] == true
|
|
721
|
+
? 'browser'
|
|
722
|
+
: (_isDesktop && _desktopOnline
|
|
723
|
+
? (widget.controller.selectedDesktopDeviceId ??
|
|
724
|
+
(_onlineDesktopDevices.isNotEmpty ? _onlineDesktopDevices.first['deviceId']?.toString() : null))
|
|
725
|
+
: (!_isBrowser && !_isDesktop && _androidOnline
|
|
726
|
+
? _androidDeviceId
|
|
727
|
+
: null)),
|
|
573
728
|
busy: _isCurrentSurfaceBusy,
|
|
574
729
|
wakingUp: !_isBrowser && !_isDesktop && _androidStarting,
|
|
575
730
|
enabled: _isBrowser || _isDesktop || _androidOnline,
|
|
@@ -578,6 +733,7 @@ class _DevicesPanelState extends State<DevicesPanel> {
|
|
|
578
733
|
: _desktopRequiresSelection,
|
|
579
734
|
onTapPoint: _handleTap,
|
|
580
735
|
onSwipe: _handleSwipe,
|
|
736
|
+
onHover: _handleHover,
|
|
581
737
|
onWakeRequested: _openPrimary,
|
|
582
738
|
),
|
|
583
739
|
if (!_isBrowser && !_isDesktop) ...<Widget>[
|
|
@@ -588,8 +744,8 @@ class _DevicesPanelState extends State<DevicesPanel> {
|
|
|
588
744
|
onAction: _runQuickAction,
|
|
589
745
|
),
|
|
590
746
|
if (kIsWeb) ...<Widget>[
|
|
591
|
-
const SizedBox(height:
|
|
592
|
-
|
|
747
|
+
const SizedBox(height: 12),
|
|
748
|
+
_AndroidActionsBox(
|
|
593
749
|
enabled: _androidOnline,
|
|
594
750
|
busy: _isCurrentSurfaceBusy,
|
|
595
751
|
onInstall: ({required filename, required bytes}) {
|
|
@@ -622,6 +778,7 @@ class _DevicesPanelState extends State<DevicesPanel> {
|
|
|
622
778
|
surface: _surface,
|
|
623
779
|
onSelect: _selectSurface,
|
|
624
780
|
),
|
|
781
|
+
],
|
|
625
782
|
],
|
|
626
783
|
),
|
|
627
784
|
),
|
|
@@ -633,13 +790,14 @@ class _DevicesPanelState extends State<DevicesPanel> {
|
|
|
633
790
|
}
|
|
634
791
|
}
|
|
635
792
|
|
|
636
|
-
enum _DeviceSurface { browser, android, desktop }
|
|
793
|
+
enum _DeviceSurface { browser, android, desktop, files }
|
|
637
794
|
|
|
638
795
|
extension _DeviceSurfaceX on _DeviceSurface {
|
|
639
796
|
String get label => switch (this) {
|
|
640
797
|
_DeviceSurface.browser => 'Browser',
|
|
641
798
|
_DeviceSurface.android => 'Phone',
|
|
642
799
|
_DeviceSurface.desktop => 'Desktop',
|
|
800
|
+
_DeviceSurface.files => 'Files',
|
|
643
801
|
};
|
|
644
802
|
|
|
645
803
|
String get helper => switch (this) {
|
|
@@ -647,12 +805,14 @@ extension _DeviceSurfaceX on _DeviceSurface {
|
|
|
647
805
|
_DeviceSurface.android => 'Tap to touch. Drag to swipe.',
|
|
648
806
|
_DeviceSurface.desktop =>
|
|
649
807
|
'Tap to click. Drag to drag windows or selections.',
|
|
808
|
+
_DeviceSurface.files => 'Browse files created by the agent.',
|
|
650
809
|
};
|
|
651
810
|
|
|
652
811
|
IconData get icon => switch (this) {
|
|
653
812
|
_DeviceSurface.browser => Icons.language_outlined,
|
|
654
813
|
_DeviceSurface.android => Icons.smartphone_outlined,
|
|
655
814
|
_DeviceSurface.desktop => Icons.computer_outlined,
|
|
815
|
+
_DeviceSurface.files => Icons.folder_outlined,
|
|
656
816
|
};
|
|
657
817
|
}
|
|
658
818
|
|
|
@@ -666,6 +826,8 @@ class _DeviceSurfaceHeader extends StatelessWidget {
|
|
|
666
826
|
required this.desktopRuntime,
|
|
667
827
|
required this.desktopDevices,
|
|
668
828
|
required this.selectedDesktopDeviceId,
|
|
829
|
+
required this.browserExtensionDevices,
|
|
830
|
+
required this.selectedBrowserExtensionTokenId,
|
|
669
831
|
required this.browserExtensionPreferred,
|
|
670
832
|
required this.browserExtensionActive,
|
|
671
833
|
required this.browserFallbackLabel,
|
|
@@ -679,6 +841,8 @@ class _DeviceSurfaceHeader extends StatelessWidget {
|
|
|
679
841
|
final Map<String, dynamic> desktopRuntime;
|
|
680
842
|
final List<Map<String, dynamic>> desktopDevices;
|
|
681
843
|
final String? selectedDesktopDeviceId;
|
|
844
|
+
final List<Map<String, dynamic>> browserExtensionDevices;
|
|
845
|
+
final String? selectedBrowserExtensionTokenId;
|
|
682
846
|
final bool browserExtensionPreferred;
|
|
683
847
|
final bool browserExtensionActive;
|
|
684
848
|
final String browserFallbackLabel;
|
|
@@ -694,12 +858,29 @@ class _DeviceSurfaceHeader extends StatelessWidget {
|
|
|
694
858
|
.where((device) => device['deviceId'] == selectedDesktopDeviceId)
|
|
695
859
|
.cast<Map<String, dynamic>?>()
|
|
696
860
|
.firstWhere((device) => device != null, orElse: () => null);
|
|
861
|
+
final selectedExtension = browserExtensionDevices
|
|
862
|
+
.where(
|
|
863
|
+
(device) => device['tokenId'] == selectedBrowserExtensionTokenId,
|
|
864
|
+
)
|
|
865
|
+
.cast<Map<String, dynamic>?>()
|
|
866
|
+
.firstWhere((device) => device != null, orElse: () => null);
|
|
867
|
+
final extensionOnlineCount = browserExtensionDevices
|
|
868
|
+
.where(
|
|
869
|
+
(device) =>
|
|
870
|
+
device['online'] == true || device['connected'] == true,
|
|
871
|
+
)
|
|
872
|
+
.length;
|
|
697
873
|
final desktopOnlineCount = desktopDevices
|
|
698
874
|
.where((device) => device['online'] == true)
|
|
699
875
|
.length;
|
|
700
876
|
final title = switch (surface) {
|
|
701
877
|
_DeviceSurface.browser =>
|
|
702
|
-
|
|
878
|
+
browserExtensionPreferred &&
|
|
879
|
+
selectedExtension?['name']?.toString().trim().isNotEmpty ==
|
|
880
|
+
true
|
|
881
|
+
? selectedExtension!['name'].toString()
|
|
882
|
+
: (browserPageInfo['title']?.toString().trim().isNotEmpty ??
|
|
883
|
+
false)
|
|
703
884
|
? browserPageInfo['title'].toString()
|
|
704
885
|
: 'Live Browser',
|
|
705
886
|
_DeviceSurface.android => 'Android Phone',
|
|
@@ -707,11 +888,16 @@ class _DeviceSurfaceHeader extends StatelessWidget {
|
|
|
707
888
|
selectedDesktop?['label']?.toString().trim().isNotEmpty == true
|
|
708
889
|
? selectedDesktop!['label'].toString()
|
|
709
890
|
: 'Desktop Companion',
|
|
891
|
+
_DeviceSurface.files => 'File Explorer',
|
|
710
892
|
};
|
|
711
893
|
final subtitle = switch (surface) {
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
?
|
|
894
|
+
_DeviceSurface.browser =>
|
|
895
|
+
browserExtensionPreferred && selectedExtension == null
|
|
896
|
+
? extensionOnlineCount > 1
|
|
897
|
+
? 'Multiple extension devices are online. Pick the browser you want to control.'
|
|
898
|
+
: 'No extension device is active. Using the $browserFallbackLabel.'
|
|
899
|
+
: browserExtensionPreferred && !browserExtensionActive
|
|
900
|
+
? 'Selected extension is offline. Using the $browserFallbackLabel.'
|
|
715
901
|
: (browserPageInfo['url']?.toString() ??
|
|
716
902
|
'Ready for navigation'),
|
|
717
903
|
_DeviceSurface.android =>
|
|
@@ -744,9 +930,13 @@ class _DeviceSurfaceHeader extends StatelessWidget {
|
|
|
744
930
|
? 'One desktop companion is online. Open the surface to fetch the latest frame.'
|
|
745
931
|
: 'No desktop companion is online. Enable Companion Mode on a signed-in desktop app.'
|
|
746
932
|
: '${selectedDesktop['platform'] ?? 'desktop'} · ${selectedDesktop['hostname'] ?? 'unknown host'}',
|
|
933
|
+
_DeviceSurface.files =>
|
|
934
|
+
'Secure per-user workspace inside the isolated VM.',
|
|
747
935
|
};
|
|
748
936
|
final statusLabel = surface == _DeviceSurface.browser
|
|
749
|
-
? browserExtensionPreferred &&
|
|
937
|
+
? browserExtensionPreferred && selectedExtension == null
|
|
938
|
+
? (extensionOnlineCount > 0 ? 'Select Device' : 'Fallback')
|
|
939
|
+
: browserExtensionPreferred && !browserExtensionActive
|
|
750
940
|
? 'Fallback'
|
|
751
941
|
: browserExtensionActive
|
|
752
942
|
? 'Extension'
|
|
@@ -759,6 +949,8 @@ class _DeviceSurfaceHeader extends StatelessWidget {
|
|
|
759
949
|
: (selectedDesktop['online'] == true
|
|
760
950
|
? 'Live'
|
|
761
951
|
: 'Offline'))
|
|
952
|
+
: surface == _DeviceSurface.files
|
|
953
|
+
? 'Isolated'
|
|
762
954
|
: (androidOnline
|
|
763
955
|
? 'Live'
|
|
764
956
|
: androidStarting
|
|
@@ -770,6 +962,8 @@ class _DeviceSurfaceHeader extends StatelessWidget {
|
|
|
770
962
|
? (selectedDesktop?['paused'] == true
|
|
771
963
|
? _warning
|
|
772
964
|
: (selectedDesktop?['online'] == true ? _success : _warning))
|
|
965
|
+
: surface == _DeviceSurface.files
|
|
966
|
+
? _success
|
|
773
967
|
: (androidOnline
|
|
774
968
|
? _success
|
|
775
969
|
: (androidStarting ? _accent : _warning));
|
|
@@ -876,16 +1070,19 @@ class _DeviceLaunchBar extends StatelessWidget {
|
|
|
876
1070
|
_DeviceSurface.android => _packageOrUrlHint,
|
|
877
1071
|
_DeviceSurface.desktop =>
|
|
878
1072
|
'Launch an app on the selected desktop (optional)',
|
|
1073
|
+
_DeviceSurface.files => 'Workspace path',
|
|
879
1074
|
};
|
|
880
1075
|
final buttonLabel = switch (surface) {
|
|
881
1076
|
_DeviceSurface.browser => 'Open',
|
|
882
1077
|
_DeviceSurface.android => starting ? 'Starting...' : 'Launch',
|
|
883
1078
|
_DeviceSurface.desktop => 'Refresh',
|
|
1079
|
+
_DeviceSurface.files => 'Refresh',
|
|
884
1080
|
};
|
|
885
1081
|
final sleepLabel = switch (surface) {
|
|
886
1082
|
_DeviceSurface.browser => 'Sleep Browser',
|
|
887
1083
|
_DeviceSurface.android => 'Sleep Phone',
|
|
888
1084
|
_DeviceSurface.desktop => 'Pause Desktop',
|
|
1085
|
+
_DeviceSurface.files => 'Close',
|
|
889
1086
|
};
|
|
890
1087
|
final narrow = MediaQuery.sizeOf(context).width < 720;
|
|
891
1088
|
|
|
@@ -899,6 +1096,8 @@ class _DeviceLaunchBar extends StatelessWidget {
|
|
|
899
1096
|
? Icons.travel_explore
|
|
900
1097
|
: surface == _DeviceSurface.desktop
|
|
901
1098
|
? Icons.apps_outlined
|
|
1099
|
+
: surface == _DeviceSurface.files
|
|
1100
|
+
? Icons.folder_outlined
|
|
902
1101
|
: Icons.open_in_new,
|
|
903
1102
|
),
|
|
904
1103
|
),
|
|
@@ -911,6 +1110,8 @@ class _DeviceLaunchBar extends StatelessWidget {
|
|
|
911
1110
|
? Icons.arrow_forward
|
|
912
1111
|
: surface == _DeviceSurface.desktop
|
|
913
1112
|
? Icons.desktop_windows_outlined
|
|
1113
|
+
: surface == _DeviceSurface.files
|
|
1114
|
+
? Icons.refresh_rounded
|
|
914
1115
|
: Icons.play_arrow,
|
|
915
1116
|
),
|
|
916
1117
|
label: Text(buttonLabel),
|
|
@@ -968,6 +1169,7 @@ class _DeviceTypeDock extends StatelessWidget {
|
|
|
968
1169
|
_DeviceSurface.browser => 'Type into the currently focused field',
|
|
969
1170
|
_DeviceSurface.android => 'Type into the current phone field',
|
|
970
1171
|
_DeviceSurface.desktop => 'Type into the focused desktop field',
|
|
1172
|
+
_DeviceSurface.files => 'Edit the selected file',
|
|
971
1173
|
};
|
|
972
1174
|
final narrow = MediaQuery.sizeOf(context).width < 720;
|
|
973
1175
|
|
|
@@ -1041,6 +1243,9 @@ class _DeviceQuickActions extends StatelessWidget {
|
|
|
1041
1243
|
_DeviceSurface.android => const <MapEntry<String, IconData>>[
|
|
1042
1244
|
MapEntry<String, IconData>('surface_refresh', Icons.refresh_rounded),
|
|
1043
1245
|
],
|
|
1246
|
+
_DeviceSurface.files => const <MapEntry<String, IconData>>[
|
|
1247
|
+
MapEntry<String, IconData>('surface_refresh', Icons.refresh_rounded),
|
|
1248
|
+
],
|
|
1044
1249
|
};
|
|
1045
1250
|
|
|
1046
1251
|
return Wrap(
|
|
@@ -1119,12 +1324,80 @@ class _AndroidNavDock extends StatelessWidget {
|
|
|
1119
1324
|
}
|
|
1120
1325
|
}
|
|
1121
1326
|
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1327
|
+
/// Tiny pill shown in the top-right corner of the preview to indicate no audio.
|
|
1328
|
+
class _MutedBadge extends StatelessWidget {
|
|
1329
|
+
const _MutedBadge();
|
|
1330
|
+
|
|
1331
|
+
@override
|
|
1332
|
+
Widget build(BuildContext context) {
|
|
1333
|
+
return Container(
|
|
1334
|
+
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 4),
|
|
1335
|
+
decoration: BoxDecoration(
|
|
1336
|
+
color: Colors.black54,
|
|
1337
|
+
borderRadius: BorderRadius.circular(8),
|
|
1338
|
+
),
|
|
1339
|
+
child: const Icon(Icons.volume_off_rounded, size: 11, color: Colors.white),
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
/// Compact expandable actions box shown beneath the Android nav dock.
|
|
1345
|
+
/// Starts with APK install; more actions can be added as tiles.
|
|
1346
|
+
class _AndroidActionsBox extends StatelessWidget {
|
|
1347
|
+
const _AndroidActionsBox({
|
|
1348
|
+
required this.enabled,
|
|
1349
|
+
required this.busy,
|
|
1350
|
+
required this.onInstall,
|
|
1126
1351
|
});
|
|
1127
1352
|
|
|
1353
|
+
final bool enabled;
|
|
1354
|
+
final bool busy;
|
|
1355
|
+
final AndroidApkInstallCallback onInstall;
|
|
1356
|
+
|
|
1357
|
+
@override
|
|
1358
|
+
Widget build(BuildContext context) {
|
|
1359
|
+
return Container(
|
|
1360
|
+
width: double.infinity,
|
|
1361
|
+
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
1362
|
+
decoration: BoxDecoration(
|
|
1363
|
+
color: _bgSecondary,
|
|
1364
|
+
borderRadius: BorderRadius.circular(18),
|
|
1365
|
+
border: Border.all(color: _borderLight),
|
|
1366
|
+
),
|
|
1367
|
+
child: Column(
|
|
1368
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1369
|
+
mainAxisSize: MainAxisSize.min,
|
|
1370
|
+
children: <Widget>[
|
|
1371
|
+
Text(
|
|
1372
|
+
'ACTIONS',
|
|
1373
|
+
style: TextStyle(
|
|
1374
|
+
fontSize: 10,
|
|
1375
|
+
fontWeight: FontWeight.w700,
|
|
1376
|
+
letterSpacing: 0.8,
|
|
1377
|
+
color: _textSecondary,
|
|
1378
|
+
),
|
|
1379
|
+
),
|
|
1380
|
+
const SizedBox(height: 8),
|
|
1381
|
+
Wrap(
|
|
1382
|
+
spacing: 8,
|
|
1383
|
+
runSpacing: 8,
|
|
1384
|
+
children: <Widget>[
|
|
1385
|
+
AndroidApkTile(
|
|
1386
|
+
enabled: enabled,
|
|
1387
|
+
busy: busy,
|
|
1388
|
+
onInstall: onInstall,
|
|
1389
|
+
),
|
|
1390
|
+
],
|
|
1391
|
+
),
|
|
1392
|
+
],
|
|
1393
|
+
),
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
class _SurfaceSwitcher extends StatelessWidget {
|
|
1399
|
+
const _SurfaceSwitcher({required this.surface, required this.onSelect});
|
|
1400
|
+
|
|
1128
1401
|
final _DeviceSurface surface;
|
|
1129
1402
|
final Future<void> Function(_DeviceSurface) onSelect;
|
|
1130
1403
|
|
|
@@ -1157,8 +1430,7 @@ class _SurfaceSwitcher extends StatelessWidget {
|
|
|
1157
1430
|
),
|
|
1158
1431
|
labelStyle: TextStyle(
|
|
1159
1432
|
color: selected ? _textPrimary : _textSecondary,
|
|
1160
|
-
fontWeight:
|
|
1161
|
-
selected ? FontWeight.w700 : FontWeight.w500,
|
|
1433
|
+
fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
|
|
1162
1434
|
),
|
|
1163
1435
|
);
|
|
1164
1436
|
}).toList(),
|
|
@@ -1179,24 +1451,30 @@ class _InteractiveSurfacePreview extends StatefulWidget {
|
|
|
1179
1451
|
required this.surface,
|
|
1180
1452
|
required this.controller,
|
|
1181
1453
|
required this.screenshotPath,
|
|
1454
|
+
required this.streamPlatform,
|
|
1455
|
+
required this.streamDeviceId,
|
|
1182
1456
|
required this.busy,
|
|
1183
1457
|
required this.wakingUp,
|
|
1184
1458
|
required this.enabled,
|
|
1185
1459
|
required this.connectRequired,
|
|
1186
1460
|
required this.onTapPoint,
|
|
1187
1461
|
required this.onSwipe,
|
|
1462
|
+
this.onHover,
|
|
1188
1463
|
required this.onWakeRequested,
|
|
1189
1464
|
});
|
|
1190
1465
|
|
|
1191
1466
|
final _DeviceSurface surface;
|
|
1192
1467
|
final NeoAgentController controller;
|
|
1193
1468
|
final String? screenshotPath;
|
|
1469
|
+
final String? streamPlatform;
|
|
1470
|
+
final String? streamDeviceId;
|
|
1194
1471
|
final bool busy;
|
|
1195
1472
|
final bool wakingUp;
|
|
1196
1473
|
final bool enabled;
|
|
1197
1474
|
final bool connectRequired;
|
|
1198
1475
|
final Future<void> Function(Offset point) onTapPoint;
|
|
1199
1476
|
final Future<void> Function(Offset start, Offset end) onSwipe;
|
|
1477
|
+
final void Function(Offset point)? onHover;
|
|
1200
1478
|
final Future<void> Function() onWakeRequested;
|
|
1201
1479
|
|
|
1202
1480
|
@override
|
|
@@ -1213,11 +1491,15 @@ class _InteractiveSurfacePreviewState
|
|
|
1213
1491
|
Object? _imageError;
|
|
1214
1492
|
Offset? _dragStart;
|
|
1215
1493
|
Offset? _dragEnd;
|
|
1494
|
+
bool _streamStarting = false;
|
|
1495
|
+
String? _activeStreamKey;
|
|
1496
|
+
String? _streamFailedKey;
|
|
1216
1497
|
|
|
1217
1498
|
@override
|
|
1218
1499
|
void initState() {
|
|
1219
1500
|
super.initState();
|
|
1220
1501
|
unawaited(_loadImage());
|
|
1502
|
+
unawaited(_syncStream());
|
|
1221
1503
|
}
|
|
1222
1504
|
|
|
1223
1505
|
@override
|
|
@@ -1226,14 +1508,131 @@ class _InteractiveSurfacePreviewState
|
|
|
1226
1508
|
if (oldWidget.screenshotPath != widget.screenshotPath) {
|
|
1227
1509
|
unawaited(_loadImage());
|
|
1228
1510
|
}
|
|
1511
|
+
if (oldWidget.streamPlatform != widget.streamPlatform ||
|
|
1512
|
+
oldWidget.streamDeviceId != widget.streamDeviceId ||
|
|
1513
|
+
oldWidget.controller.streamSocket != widget.controller.streamSocket) {
|
|
1514
|
+
unawaited(_syncStream());
|
|
1515
|
+
}
|
|
1229
1516
|
}
|
|
1230
1517
|
|
|
1231
1518
|
@override
|
|
1232
1519
|
void dispose() {
|
|
1520
|
+
unawaited(_stopActiveStream());
|
|
1233
1521
|
_detachImageListener();
|
|
1234
1522
|
super.dispose();
|
|
1235
1523
|
}
|
|
1236
1524
|
|
|
1525
|
+
String? get _requestedStreamKey {
|
|
1526
|
+
final platform = widget.streamPlatform?.trim();
|
|
1527
|
+
final deviceId = widget.streamDeviceId?.trim();
|
|
1528
|
+
if (platform == null ||
|
|
1529
|
+
platform.isEmpty ||
|
|
1530
|
+
deviceId == null ||
|
|
1531
|
+
deviceId.isEmpty ||
|
|
1532
|
+
widget.controller.streamSocket == null) {
|
|
1533
|
+
return null;
|
|
1534
|
+
}
|
|
1535
|
+
return '$platform:$deviceId';
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
Future<void> _syncStream() async {
|
|
1539
|
+
final requested = _requestedStreamKey;
|
|
1540
|
+
if (_activeStreamKey == requested || _streamStarting) {
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
if (requested != _streamFailedKey) {
|
|
1544
|
+
_streamFailedKey = null;
|
|
1545
|
+
}
|
|
1546
|
+
await _stopActiveStream();
|
|
1547
|
+
if (requested == null) {
|
|
1548
|
+
return;
|
|
1549
|
+
}
|
|
1550
|
+
final parts = requested.split(':');
|
|
1551
|
+
_streamStarting = true;
|
|
1552
|
+
try {
|
|
1553
|
+
await widget.controller.startStreamRuntime(
|
|
1554
|
+
platform: parts[0],
|
|
1555
|
+
deviceId: parts.sublist(1).join(':'),
|
|
1556
|
+
fps: 10,
|
|
1557
|
+
quality: 70,
|
|
1558
|
+
);
|
|
1559
|
+
if (mounted && _requestedStreamKey == requested) {
|
|
1560
|
+
_activeStreamKey = requested;
|
|
1561
|
+
_streamFailedKey = null;
|
|
1562
|
+
} else {
|
|
1563
|
+
await widget.controller.stopStreamRuntime(
|
|
1564
|
+
platform: parts[0],
|
|
1565
|
+
deviceId: parts.sublist(1).join(':'),
|
|
1566
|
+
);
|
|
1567
|
+
}
|
|
1568
|
+
} catch (_) {
|
|
1569
|
+
if (mounted) {
|
|
1570
|
+
_streamFailedKey = requested;
|
|
1571
|
+
}
|
|
1572
|
+
if (mounted && (widget.screenshotPath ?? '').isEmpty) {
|
|
1573
|
+
unawaited(_loadImage());
|
|
1574
|
+
}
|
|
1575
|
+
} finally {
|
|
1576
|
+
_streamStarting = false;
|
|
1577
|
+
if (mounted &&
|
|
1578
|
+
_activeStreamKey != _requestedStreamKey &&
|
|
1579
|
+
_streamFailedKey != _requestedStreamKey) {
|
|
1580
|
+
unawaited(_syncStream());
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
Future<void> _stopActiveStream() async {
|
|
1586
|
+
final active = _activeStreamKey;
|
|
1587
|
+
_activeStreamKey = null;
|
|
1588
|
+
if (active == null) {
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
final parts = active.split(':');
|
|
1592
|
+
try {
|
|
1593
|
+
await widget.controller.stopStreamRuntime(
|
|
1594
|
+
platform: parts[0],
|
|
1595
|
+
deviceId: parts.sublist(1).join(':'),
|
|
1596
|
+
);
|
|
1597
|
+
} catch (_) {}
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
void _handleStreamFirstFrame(String streamKey) {
|
|
1601
|
+
if (!mounted || _requestedStreamKey != streamKey) {
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
if (_streamFailedKey == streamKey) {
|
|
1605
|
+
setState(() => _streamFailedKey = null);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
void _handleStreamFrameTimeout(String streamKey) {
|
|
1610
|
+
if (!mounted || _requestedStreamKey != streamKey) {
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
setState(() => _streamFailedKey = streamKey);
|
|
1614
|
+
unawaited(_stopActiveStream());
|
|
1615
|
+
if ((widget.screenshotPath ?? '').isEmpty) {
|
|
1616
|
+
unawaited(_refreshStaticFrame());
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
Future<void> _refreshStaticFrame() async {
|
|
1621
|
+
switch (widget.surface) {
|
|
1622
|
+
case _DeviceSurface.browser:
|
|
1623
|
+
await widget.controller.screenshotBrowserRuntime();
|
|
1624
|
+
break;
|
|
1625
|
+
case _DeviceSurface.android:
|
|
1626
|
+
await widget.controller.screenshotAndroidRuntime();
|
|
1627
|
+
break;
|
|
1628
|
+
case _DeviceSurface.desktop:
|
|
1629
|
+
await widget.controller.screenshotDesktopRuntime();
|
|
1630
|
+
break;
|
|
1631
|
+
case _DeviceSurface.files:
|
|
1632
|
+
break;
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1237
1636
|
void _detachImageListener() {
|
|
1238
1637
|
if (_imageStream != null && _imageListener != null) {
|
|
1239
1638
|
_imageStream!.removeListener(_imageListener!);
|
|
@@ -1339,6 +1738,7 @@ class _InteractiveSurfacePreviewState
|
|
|
1339
1738
|
_DeviceSurface.browser => 16 / 10,
|
|
1340
1739
|
_DeviceSurface.android => 10 / 16,
|
|
1341
1740
|
_DeviceSurface.desktop => 16 / 10,
|
|
1741
|
+
_DeviceSurface.files => 16 / 10,
|
|
1342
1742
|
};
|
|
1343
1743
|
|
|
1344
1744
|
return Container(
|
|
@@ -1368,6 +1768,81 @@ class _InteractiveSurfacePreviewState
|
|
|
1368
1768
|
constraints.maxWidth,
|
|
1369
1769
|
constraints.maxHeight,
|
|
1370
1770
|
);
|
|
1771
|
+
final socket = widget.controller.streamSocket;
|
|
1772
|
+
final streamPlatform = widget.streamPlatform;
|
|
1773
|
+
final streamDeviceId = widget.streamDeviceId;
|
|
1774
|
+
final streamKey = _requestedStreamKey;
|
|
1775
|
+
if (socket != null &&
|
|
1776
|
+
streamPlatform != null &&
|
|
1777
|
+
streamPlatform.isNotEmpty &&
|
|
1778
|
+
streamDeviceId != null &&
|
|
1779
|
+
streamDeviceId.isNotEmpty &&
|
|
1780
|
+
streamKey != _streamFailedKey) {
|
|
1781
|
+
final activeStreamKey = streamKey!;
|
|
1782
|
+
return Stack(
|
|
1783
|
+
fit: StackFit.expand,
|
|
1784
|
+
children: <Widget>[
|
|
1785
|
+
Container(color: _bgSecondary),
|
|
1786
|
+
StreamRenderer(
|
|
1787
|
+
socket: socket,
|
|
1788
|
+
deviceId: streamDeviceId,
|
|
1789
|
+
platform: streamPlatform,
|
|
1790
|
+
remoteResolution: _pixelSize,
|
|
1791
|
+
onFirstFrame: () =>
|
|
1792
|
+
_handleStreamFirstFrame(activeStreamKey),
|
|
1793
|
+
onFrameTimeout: () =>
|
|
1794
|
+
_handleStreamFrameTimeout(activeStreamKey),
|
|
1795
|
+
onTap: widget.busy
|
|
1796
|
+
? null
|
|
1797
|
+
: (x, y) => unawaited(
|
|
1798
|
+
widget.onTapPoint(Offset(x, y)),
|
|
1799
|
+
),
|
|
1800
|
+
onSwipe: widget.busy
|
|
1801
|
+
? null
|
|
1802
|
+
: (x1, y1, x2, y2) => unawaited(
|
|
1803
|
+
widget.onSwipe(
|
|
1804
|
+
Offset(x1, y1),
|
|
1805
|
+
Offset(x2, y2),
|
|
1806
|
+
),
|
|
1807
|
+
),
|
|
1808
|
+
onHover: widget.busy
|
|
1809
|
+
? null
|
|
1810
|
+
: (x, y) => widget.onHover?.call(Offset(x, y)),
|
|
1811
|
+
),
|
|
1812
|
+
const Positioned(
|
|
1813
|
+
top: 8,
|
|
1814
|
+
right: 8,
|
|
1815
|
+
child: Opacity(
|
|
1816
|
+
opacity: 0.45,
|
|
1817
|
+
child: _MutedBadge(),
|
|
1818
|
+
),
|
|
1819
|
+
),
|
|
1820
|
+
Positioned(
|
|
1821
|
+
left: 12,
|
|
1822
|
+
right: 12,
|
|
1823
|
+
bottom: 12,
|
|
1824
|
+
child: Container(
|
|
1825
|
+
padding: const EdgeInsets.symmetric(
|
|
1826
|
+
horizontal: 12,
|
|
1827
|
+
vertical: 10,
|
|
1828
|
+
),
|
|
1829
|
+
decoration: BoxDecoration(
|
|
1830
|
+
color: const Color(0xB205080D),
|
|
1831
|
+
borderRadius: BorderRadius.circular(14),
|
|
1832
|
+
border: Border.all(color: _borderLight),
|
|
1833
|
+
),
|
|
1834
|
+
child: Text(
|
|
1835
|
+
widget.surface.helper,
|
|
1836
|
+
textAlign: TextAlign.center,
|
|
1837
|
+
style: TextStyle(color: _textPrimary),
|
|
1838
|
+
),
|
|
1839
|
+
),
|
|
1840
|
+
),
|
|
1841
|
+
if (widget.busy)
|
|
1842
|
+
const Center(child: CircularProgressIndicator()),
|
|
1843
|
+
],
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1371
1846
|
if (!hasImage) {
|
|
1372
1847
|
return _EmptySurfaceState(
|
|
1373
1848
|
surface: widget.surface,
|
|
@@ -1393,84 +1868,96 @@ class _InteractiveSurfacePreviewState
|
|
|
1393
1868
|
}
|
|
1394
1869
|
return Semantics(
|
|
1395
1870
|
button: true,
|
|
1396
|
-
label:
|
|
1871
|
+
label:
|
|
1872
|
+
'Device surface preview — tap to interact, swipe to scroll',
|
|
1397
1873
|
child: GestureDetector(
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
decoration: BoxDecoration(
|
|
1458
|
-
color: const Color(0xB205080D),
|
|
1459
|
-
borderRadius: BorderRadius.circular(14),
|
|
1460
|
-
border: Border.all(color: _borderLight),
|
|
1874
|
+
onTapUp: widget.busy
|
|
1875
|
+
? null
|
|
1876
|
+
: (details) async {
|
|
1877
|
+
final point = _mapToPixels(
|
|
1878
|
+
details.localPosition,
|
|
1879
|
+
boxSize,
|
|
1880
|
+
);
|
|
1881
|
+
if (point != null) {
|
|
1882
|
+
await widget.onTapPoint(point);
|
|
1883
|
+
}
|
|
1884
|
+
},
|
|
1885
|
+
onPanStart: widget.busy
|
|
1886
|
+
? null
|
|
1887
|
+
: (details) {
|
|
1888
|
+
_dragStart = details.localPosition;
|
|
1889
|
+
_dragEnd = details.localPosition;
|
|
1890
|
+
},
|
|
1891
|
+
onPanUpdate: widget.busy
|
|
1892
|
+
? null
|
|
1893
|
+
: (details) {
|
|
1894
|
+
_dragEnd = details.localPosition;
|
|
1895
|
+
},
|
|
1896
|
+
onPanEnd: widget.busy
|
|
1897
|
+
? null
|
|
1898
|
+
: (_) async {
|
|
1899
|
+
final start = _dragStart;
|
|
1900
|
+
final end = _dragEnd;
|
|
1901
|
+
_dragStart = null;
|
|
1902
|
+
_dragEnd = null;
|
|
1903
|
+
if (start == null || end == null) {
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
if ((start - end).distance < 12) {
|
|
1907
|
+
return;
|
|
1908
|
+
}
|
|
1909
|
+
final mappedStart = _mapToPixels(
|
|
1910
|
+
start,
|
|
1911
|
+
boxSize,
|
|
1912
|
+
);
|
|
1913
|
+
final mappedEnd = _mapToPixels(end, boxSize);
|
|
1914
|
+
if (mappedStart != null && mappedEnd != null) {
|
|
1915
|
+
await widget.onSwipe(mappedStart, mappedEnd);
|
|
1916
|
+
}
|
|
1917
|
+
},
|
|
1918
|
+
child: Stack(
|
|
1919
|
+
fit: StackFit.expand,
|
|
1920
|
+
children: <Widget>[
|
|
1921
|
+
Container(color: _bgSecondary),
|
|
1922
|
+
Image.memory(
|
|
1923
|
+
imageBytes,
|
|
1924
|
+
fit: BoxFit.contain,
|
|
1925
|
+
gaplessPlayback: true,
|
|
1926
|
+
),
|
|
1927
|
+
const Positioned(
|
|
1928
|
+
top: 8,
|
|
1929
|
+
right: 8,
|
|
1930
|
+
child: Opacity(
|
|
1931
|
+
opacity: 0.45,
|
|
1932
|
+
child: _MutedBadge(),
|
|
1461
1933
|
),
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1934
|
+
),
|
|
1935
|
+
Positioned(
|
|
1936
|
+
left: 12,
|
|
1937
|
+
right: 12,
|
|
1938
|
+
bottom: 12,
|
|
1939
|
+
child: Container(
|
|
1940
|
+
padding: const EdgeInsets.symmetric(
|
|
1941
|
+
horizontal: 12,
|
|
1942
|
+
vertical: 10,
|
|
1943
|
+
),
|
|
1944
|
+
decoration: BoxDecoration(
|
|
1945
|
+
color: const Color(0xB205080D),
|
|
1946
|
+
borderRadius: BorderRadius.circular(14),
|
|
1947
|
+
border: Border.all(color: _borderLight),
|
|
1948
|
+
),
|
|
1949
|
+
child: Text(
|
|
1950
|
+
widget.surface.helper,
|
|
1951
|
+
textAlign: TextAlign.center,
|
|
1952
|
+
style: TextStyle(color: _textPrimary),
|
|
1953
|
+
),
|
|
1466
1954
|
),
|
|
1467
1955
|
),
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1956
|
+
if (widget.busy)
|
|
1957
|
+
const Center(child: CircularProgressIndicator()),
|
|
1958
|
+
],
|
|
1959
|
+
),
|
|
1472
1960
|
),
|
|
1473
|
-
),
|
|
1474
1961
|
);
|
|
1475
1962
|
},
|
|
1476
1963
|
),
|
|
@@ -1517,6 +2004,7 @@ class _EmptySurfaceState extends StatelessWidget {
|
|
|
1517
2004
|
connectRequired
|
|
1518
2005
|
? 'Select Desktop'
|
|
1519
2006
|
: (busy ? 'Refreshing Desktop...' : 'Refresh Desktop'),
|
|
2007
|
+
_DeviceSurface.files => busy ? 'Loading Files...' : 'Refresh Files',
|
|
1520
2008
|
};
|
|
1521
2009
|
final message = switch ((surface, busy, isLoadingPreview)) {
|
|
1522
2010
|
(_DeviceSurface.browser, true, _) =>
|
|
@@ -1531,6 +2019,8 @@ class _EmptySurfaceState extends StatelessWidget {
|
|
|
1531
2019
|
'Waking the phone and downloading the first preview. This can take a little while.',
|
|
1532
2020
|
(_DeviceSurface.android, false, true) =>
|
|
1533
2021
|
'Downloading the latest phone preview...',
|
|
2022
|
+
(_DeviceSurface.files, true, _) => 'Loading workspace files...',
|
|
2023
|
+
(_DeviceSurface.files, false, true) => 'Loading workspace files...',
|
|
1534
2024
|
_ =>
|
|
1535
2025
|
surface == _DeviceSurface.browser
|
|
1536
2026
|
? connectRequired
|
|
@@ -1592,9 +2082,6 @@ class _EmptySurfaceState extends StatelessWidget {
|
|
|
1592
2082
|
}
|
|
1593
2083
|
}
|
|
1594
2084
|
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
2085
|
class _DeviceFieldRow extends StatelessWidget {
|
|
1599
2086
|
const _DeviceFieldRow({required this.children});
|
|
1600
2087
|
|
|
@@ -1741,7 +2228,9 @@ class _ExtensionStatusBar extends StatelessWidget {
|
|
|
1741
2228
|
child: Row(
|
|
1742
2229
|
children: <Widget>[
|
|
1743
2230
|
_DotStatus(
|
|
1744
|
-
label: connected
|
|
2231
|
+
label: connected
|
|
2232
|
+
? 'Extension connected'
|
|
2233
|
+
: 'Extension not connected',
|
|
1745
2234
|
color: connected ? _success : _warning,
|
|
1746
2235
|
),
|
|
1747
2236
|
const Spacer(),
|
|
@@ -1770,6 +2259,246 @@ class _ExtensionStatusBar extends StatelessWidget {
|
|
|
1770
2259
|
}
|
|
1771
2260
|
}
|
|
1772
2261
|
|
|
2262
|
+
class _WorkspaceExplorer extends StatefulWidget {
|
|
2263
|
+
const _WorkspaceExplorer({
|
|
2264
|
+
required this.controller,
|
|
2265
|
+
required this.editorController,
|
|
2266
|
+
});
|
|
2267
|
+
|
|
2268
|
+
final NeoAgentController controller;
|
|
2269
|
+
final TextEditingController editorController;
|
|
2270
|
+
|
|
2271
|
+
@override
|
|
2272
|
+
State<_WorkspaceExplorer> createState() => _WorkspaceExplorerState();
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
class _WorkspaceExplorerState extends State<_WorkspaceExplorer> {
|
|
2276
|
+
String? _syncedPath;
|
|
2277
|
+
|
|
2278
|
+
@override
|
|
2279
|
+
void initState() {
|
|
2280
|
+
super.initState();
|
|
2281
|
+
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
2282
|
+
if (!mounted || widget.controller.workspaceEntries.isNotEmpty) {
|
|
2283
|
+
return;
|
|
2284
|
+
}
|
|
2285
|
+
unawaited(widget.controller.refreshWorkspaceFiles());
|
|
2286
|
+
});
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
@override
|
|
2290
|
+
void didUpdateWidget(covariant _WorkspaceExplorer oldWidget) {
|
|
2291
|
+
super.didUpdateWidget(oldWidget);
|
|
2292
|
+
final selectedPath = widget.controller.workspaceSelectedFilePath;
|
|
2293
|
+
if (selectedPath != _syncedPath) {
|
|
2294
|
+
_syncedPath = selectedPath;
|
|
2295
|
+
widget.editorController.text = widget.controller.workspaceEditorContent;
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
Future<void> _openEntry(Map<String, dynamic> entry) async {
|
|
2300
|
+
final path = entry['path']?.toString() ?? '';
|
|
2301
|
+
if (path.isEmpty && entry['name']?.toString() != '') {
|
|
2302
|
+
return;
|
|
2303
|
+
}
|
|
2304
|
+
if (entry['type']?.toString() == 'directory') {
|
|
2305
|
+
await widget.controller.openWorkspaceDirectory(path);
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
await widget.controller.openWorkspaceFile(path);
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
@override
|
|
2312
|
+
Widget build(BuildContext context) {
|
|
2313
|
+
final controller = widget.controller;
|
|
2314
|
+
final entries = controller.workspaceEntries;
|
|
2315
|
+
final currentPath = controller.workspaceCurrentPath;
|
|
2316
|
+
final selectedPath = controller.workspaceSelectedFilePath;
|
|
2317
|
+
final busy =
|
|
2318
|
+
controller.isLoadingWorkspaceFiles || controller.isSavingWorkspaceFile;
|
|
2319
|
+
final canGoUp = currentPath.trim().isNotEmpty;
|
|
2320
|
+
final parentPath = currentPath.contains('/')
|
|
2321
|
+
? currentPath.substring(0, currentPath.lastIndexOf('/'))
|
|
2322
|
+
: '';
|
|
2323
|
+
|
|
2324
|
+
return Container(
|
|
2325
|
+
width: double.infinity,
|
|
2326
|
+
padding: const EdgeInsets.all(14),
|
|
2327
|
+
decoration: BoxDecoration(
|
|
2328
|
+
color: _bgSecondary,
|
|
2329
|
+
borderRadius: BorderRadius.circular(18),
|
|
2330
|
+
border: Border.all(color: _borderLight),
|
|
2331
|
+
),
|
|
2332
|
+
child: Column(
|
|
2333
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
2334
|
+
children: <Widget>[
|
|
2335
|
+
Row(
|
|
2336
|
+
children: <Widget>[
|
|
2337
|
+
Expanded(
|
|
2338
|
+
child: Text(
|
|
2339
|
+
currentPath.isEmpty ? '/' : '/$currentPath',
|
|
2340
|
+
maxLines: 1,
|
|
2341
|
+
overflow: TextOverflow.ellipsis,
|
|
2342
|
+
style: TextStyle(
|
|
2343
|
+
color: _textPrimary,
|
|
2344
|
+
fontWeight: FontWeight.w700,
|
|
2345
|
+
),
|
|
2346
|
+
),
|
|
2347
|
+
),
|
|
2348
|
+
IconButton.filledTonal(
|
|
2349
|
+
tooltip: 'Parent folder',
|
|
2350
|
+
onPressed: busy || !canGoUp
|
|
2351
|
+
? null
|
|
2352
|
+
: () => controller.openWorkspaceDirectory(parentPath),
|
|
2353
|
+
icon: const Icon(Icons.arrow_upward_rounded),
|
|
2354
|
+
),
|
|
2355
|
+
const SizedBox(width: 8),
|
|
2356
|
+
IconButton.filledTonal(
|
|
2357
|
+
tooltip: 'Refresh',
|
|
2358
|
+
onPressed: busy ? null : controller.refreshWorkspaceFiles,
|
|
2359
|
+
icon: const Icon(Icons.refresh_rounded),
|
|
2360
|
+
),
|
|
2361
|
+
],
|
|
2362
|
+
),
|
|
2363
|
+
const SizedBox(height: 12),
|
|
2364
|
+
Container(
|
|
2365
|
+
constraints: const BoxConstraints(maxHeight: 260),
|
|
2366
|
+
decoration: BoxDecoration(
|
|
2367
|
+
color: _bgCard,
|
|
2368
|
+
borderRadius: BorderRadius.circular(14),
|
|
2369
|
+
border: Border.all(color: _borderLight),
|
|
2370
|
+
),
|
|
2371
|
+
child: entries.isEmpty
|
|
2372
|
+
? Center(
|
|
2373
|
+
child: Padding(
|
|
2374
|
+
padding: const EdgeInsets.all(24),
|
|
2375
|
+
child: Text(
|
|
2376
|
+
controller.isLoadingWorkspaceFiles
|
|
2377
|
+
? 'Loading files...'
|
|
2378
|
+
: 'No files in this folder.',
|
|
2379
|
+
style: TextStyle(color: _textSecondary),
|
|
2380
|
+
),
|
|
2381
|
+
),
|
|
2382
|
+
)
|
|
2383
|
+
: ListView.separated(
|
|
2384
|
+
shrinkWrap: true,
|
|
2385
|
+
itemCount: entries.length,
|
|
2386
|
+
separatorBuilder: (_, __) => Divider(
|
|
2387
|
+
height: 1,
|
|
2388
|
+
color: _borderLight,
|
|
2389
|
+
),
|
|
2390
|
+
itemBuilder: (context, index) {
|
|
2391
|
+
final entry = entries[index];
|
|
2392
|
+
final type = entry['type']?.toString() ?? 'file';
|
|
2393
|
+
final path = entry['path']?.toString() ?? '';
|
|
2394
|
+
final selected = selectedPath == path;
|
|
2395
|
+
return ListTile(
|
|
2396
|
+
dense: true,
|
|
2397
|
+
selected: selected,
|
|
2398
|
+
leading: Icon(
|
|
2399
|
+
type == 'directory'
|
|
2400
|
+
? Icons.folder_outlined
|
|
2401
|
+
: Icons.insert_drive_file_outlined,
|
|
2402
|
+
color: type == 'directory' ? _accent : _textSecondary,
|
|
2403
|
+
),
|
|
2404
|
+
title: Text(
|
|
2405
|
+
entry['name']?.toString() ?? path,
|
|
2406
|
+
maxLines: 1,
|
|
2407
|
+
overflow: TextOverflow.ellipsis,
|
|
2408
|
+
),
|
|
2409
|
+
subtitle: type == 'file'
|
|
2410
|
+
? Text(_formatWorkspaceBytes(entry['size']))
|
|
2411
|
+
: null,
|
|
2412
|
+
trailing: type == 'file'
|
|
2413
|
+
? IconButton(
|
|
2414
|
+
tooltip: 'Download',
|
|
2415
|
+
onPressed: busy || path.isEmpty
|
|
2416
|
+
? null
|
|
2417
|
+
: () => controller.downloadWorkspaceFile(
|
|
2418
|
+
path,
|
|
2419
|
+
),
|
|
2420
|
+
icon: const Icon(Icons.download_outlined),
|
|
2421
|
+
)
|
|
2422
|
+
: const Icon(Icons.chevron_right_rounded),
|
|
2423
|
+
onTap: busy ? null : () => _openEntry(entry),
|
|
2424
|
+
);
|
|
2425
|
+
},
|
|
2426
|
+
),
|
|
2427
|
+
),
|
|
2428
|
+
const SizedBox(height: 14),
|
|
2429
|
+
Row(
|
|
2430
|
+
children: <Widget>[
|
|
2431
|
+
Expanded(
|
|
2432
|
+
child: Text(
|
|
2433
|
+
selectedPath ?? 'No file selected',
|
|
2434
|
+
maxLines: 1,
|
|
2435
|
+
overflow: TextOverflow.ellipsis,
|
|
2436
|
+
style: TextStyle(
|
|
2437
|
+
color: selectedPath == null ? _textSecondary : _textPrimary,
|
|
2438
|
+
fontWeight: FontWeight.w700,
|
|
2439
|
+
),
|
|
2440
|
+
),
|
|
2441
|
+
),
|
|
2442
|
+
OutlinedButton.icon(
|
|
2443
|
+
onPressed: busy || selectedPath == null
|
|
2444
|
+
? null
|
|
2445
|
+
: () => controller.downloadWorkspaceFile(selectedPath),
|
|
2446
|
+
icon: const Icon(Icons.download_outlined, size: 18),
|
|
2447
|
+
label: const Text('Download'),
|
|
2448
|
+
),
|
|
2449
|
+
const SizedBox(width: 8),
|
|
2450
|
+
FilledButton.icon(
|
|
2451
|
+
onPressed: busy || selectedPath == null
|
|
2452
|
+
? null
|
|
2453
|
+
: () => controller.saveWorkspaceFile(
|
|
2454
|
+
widget.editorController.text,
|
|
2455
|
+
),
|
|
2456
|
+
icon: controller.isSavingWorkspaceFile
|
|
2457
|
+
? const SizedBox(
|
|
2458
|
+
width: 16,
|
|
2459
|
+
height: 16,
|
|
2460
|
+
child: CircularProgressIndicator(strokeWidth: 2),
|
|
2461
|
+
)
|
|
2462
|
+
: const Icon(Icons.save_outlined, size: 18),
|
|
2463
|
+
label: const Text('Save'),
|
|
2464
|
+
),
|
|
2465
|
+
],
|
|
2466
|
+
),
|
|
2467
|
+
const SizedBox(height: 10),
|
|
2468
|
+
TextField(
|
|
2469
|
+
controller: widget.editorController,
|
|
2470
|
+
enabled: selectedPath != null && !controller.isSavingWorkspaceFile,
|
|
2471
|
+
minLines: 12,
|
|
2472
|
+
maxLines: 18,
|
|
2473
|
+
style: const TextStyle(
|
|
2474
|
+
fontFamily: 'monospace',
|
|
2475
|
+
fontSize: 13,
|
|
2476
|
+
height: 1.35,
|
|
2477
|
+
),
|
|
2478
|
+
decoration: InputDecoration(
|
|
2479
|
+
hintText: selectedPath == null
|
|
2480
|
+
? 'Select a file to view or edit it.'
|
|
2481
|
+
: 'File contents',
|
|
2482
|
+
alignLabelWithHint: true,
|
|
2483
|
+
),
|
|
2484
|
+
),
|
|
2485
|
+
],
|
|
2486
|
+
),
|
|
2487
|
+
);
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
String _formatWorkspaceBytes(Object? value) {
|
|
2492
|
+
final bytes = value is num ? value.toDouble() : double.tryParse('$value') ?? 0;
|
|
2493
|
+
if (bytes >= 1024 * 1024) {
|
|
2494
|
+
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
|
|
2495
|
+
}
|
|
2496
|
+
if (bytes >= 1024) {
|
|
2497
|
+
return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
|
2498
|
+
}
|
|
2499
|
+
return '${bytes.round()} B';
|
|
2500
|
+
}
|
|
2501
|
+
|
|
1773
2502
|
class _ResultBlock extends StatelessWidget {
|
|
1774
2503
|
const _ResultBlock({required this.label, required this.value});
|
|
1775
2504
|
|
|
@@ -1794,7 +2523,7 @@ class _ResultBlock extends StatelessWidget {
|
|
|
1794
2523
|
SelectableText(
|
|
1795
2524
|
value,
|
|
1796
2525
|
style: TextStyle(
|
|
1797
|
-
fontFamily: GoogleFonts.
|
|
2526
|
+
fontFamily: GoogleFonts.geistMono().fontFamily,
|
|
1798
2527
|
fontSize: 12,
|
|
1799
2528
|
color: _textSecondary,
|
|
1800
2529
|
height: 1.5,
|