neoagent 2.4.1-beta.9 → 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 +36 -4
- 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 -3
- package/extensions/chrome-browser/popup.html +5 -1
- package/extensions/chrome-browser/popup.js +13 -0
- 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 +2 -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 +197 -42
- package/flutter_app/lib/main_devices.dart +436 -4
- 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 +673 -871
- 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 +46 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +30 -7
- package/flutter_app/lib/src/desktop_companion_io.dart +71 -1
- package/flutter_app/lib/src/security/password_strength.dart +84 -0
- package/flutter_app/lib/src/stream_renderer.dart +42 -3
- 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/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 +88537 -85936
- 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/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 +10 -2
- package/server/routes/stream.js +1 -1
- 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 +78 -13
- package/server/services/browser/extension/registry.js +94 -9
- 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 -53
- 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/streaming/android-stream.js +293 -40
- 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 &&
|
|
@@ -180,6 +186,13 @@ class _DevicesPanelState extends State<DevicesPanel> {
|
|
|
180
186
|
return;
|
|
181
187
|
}
|
|
182
188
|
|
|
189
|
+
if (_isFiles) {
|
|
190
|
+
if (controller.workspaceEntries.isEmpty) {
|
|
191
|
+
await controller.refreshWorkspaceFiles();
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
183
196
|
if (_androidOnline && (controller.androidScreenshotPath ?? '').isEmpty) {
|
|
184
197
|
await controller.screenshotAndroidRuntime();
|
|
185
198
|
}
|
|
@@ -201,6 +214,9 @@ class _DevicesPanelState extends State<DevicesPanel> {
|
|
|
201
214
|
await widget.controller.refreshDesktopFrameRuntime();
|
|
202
215
|
return;
|
|
203
216
|
}
|
|
217
|
+
if (_isFiles) {
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
204
220
|
if (_androidStarting) {
|
|
205
221
|
await widget.controller.refreshDevices();
|
|
206
222
|
if (_androidOnline &&
|
|
@@ -258,6 +274,11 @@ class _DevicesPanelState extends State<DevicesPanel> {
|
|
|
258
274
|
return;
|
|
259
275
|
}
|
|
260
276
|
|
|
277
|
+
if (_isFiles) {
|
|
278
|
+
await controller.refreshWorkspaceFiles();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
|
|
261
282
|
if (!_androidOnline) {
|
|
262
283
|
await controller.startAndroidRuntime();
|
|
263
284
|
await widget.controller.refreshDevices();
|
|
@@ -299,6 +320,9 @@ class _DevicesPanelState extends State<DevicesPanel> {
|
|
|
299
320
|
}
|
|
300
321
|
return;
|
|
301
322
|
}
|
|
323
|
+
if (_isFiles) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
302
326
|
if (!_androidOnline) {
|
|
303
327
|
return;
|
|
304
328
|
}
|
|
@@ -314,6 +338,8 @@ class _DevicesPanelState extends State<DevicesPanel> {
|
|
|
314
338
|
await widget.controller.typeBrowserTextRuntime(text, pressEnter: true);
|
|
315
339
|
} else if (_isDesktop) {
|
|
316
340
|
await widget.controller.typeDesktopRuntime(text, pressEnter: true);
|
|
341
|
+
} else if (_isFiles) {
|
|
342
|
+
return;
|
|
317
343
|
} else {
|
|
318
344
|
await widget.controller.typeAndroidRuntime(<String, dynamic>{
|
|
319
345
|
'text': text,
|
|
@@ -513,6 +539,18 @@ class _DevicesPanelState extends State<DevicesPanel> {
|
|
|
513
539
|
browserExtensionActive: usingExtension,
|
|
514
540
|
browserFallbackLabel: browserFallbackLabel,
|
|
515
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>[
|
|
516
554
|
if (_isBrowser && prefersExtension) ...<Widget>[
|
|
517
555
|
const SizedBox(height: 14),
|
|
518
556
|
if (_browserExtensionDevices.isNotEmpty) ...<Widget>[
|
|
@@ -706,8 +744,8 @@ class _DevicesPanelState extends State<DevicesPanel> {
|
|
|
706
744
|
onAction: _runQuickAction,
|
|
707
745
|
),
|
|
708
746
|
if (kIsWeb) ...<Widget>[
|
|
709
|
-
const SizedBox(height:
|
|
710
|
-
|
|
747
|
+
const SizedBox(height: 12),
|
|
748
|
+
_AndroidActionsBox(
|
|
711
749
|
enabled: _androidOnline,
|
|
712
750
|
busy: _isCurrentSurfaceBusy,
|
|
713
751
|
onInstall: ({required filename, required bytes}) {
|
|
@@ -740,6 +778,7 @@ class _DevicesPanelState extends State<DevicesPanel> {
|
|
|
740
778
|
surface: _surface,
|
|
741
779
|
onSelect: _selectSurface,
|
|
742
780
|
),
|
|
781
|
+
],
|
|
743
782
|
],
|
|
744
783
|
),
|
|
745
784
|
),
|
|
@@ -751,13 +790,14 @@ class _DevicesPanelState extends State<DevicesPanel> {
|
|
|
751
790
|
}
|
|
752
791
|
}
|
|
753
792
|
|
|
754
|
-
enum _DeviceSurface { browser, android, desktop }
|
|
793
|
+
enum _DeviceSurface { browser, android, desktop, files }
|
|
755
794
|
|
|
756
795
|
extension _DeviceSurfaceX on _DeviceSurface {
|
|
757
796
|
String get label => switch (this) {
|
|
758
797
|
_DeviceSurface.browser => 'Browser',
|
|
759
798
|
_DeviceSurface.android => 'Phone',
|
|
760
799
|
_DeviceSurface.desktop => 'Desktop',
|
|
800
|
+
_DeviceSurface.files => 'Files',
|
|
761
801
|
};
|
|
762
802
|
|
|
763
803
|
String get helper => switch (this) {
|
|
@@ -765,12 +805,14 @@ extension _DeviceSurfaceX on _DeviceSurface {
|
|
|
765
805
|
_DeviceSurface.android => 'Tap to touch. Drag to swipe.',
|
|
766
806
|
_DeviceSurface.desktop =>
|
|
767
807
|
'Tap to click. Drag to drag windows or selections.',
|
|
808
|
+
_DeviceSurface.files => 'Browse files created by the agent.',
|
|
768
809
|
};
|
|
769
810
|
|
|
770
811
|
IconData get icon => switch (this) {
|
|
771
812
|
_DeviceSurface.browser => Icons.language_outlined,
|
|
772
813
|
_DeviceSurface.android => Icons.smartphone_outlined,
|
|
773
814
|
_DeviceSurface.desktop => Icons.computer_outlined,
|
|
815
|
+
_DeviceSurface.files => Icons.folder_outlined,
|
|
774
816
|
};
|
|
775
817
|
}
|
|
776
818
|
|
|
@@ -846,6 +888,7 @@ class _DeviceSurfaceHeader extends StatelessWidget {
|
|
|
846
888
|
selectedDesktop?['label']?.toString().trim().isNotEmpty == true
|
|
847
889
|
? selectedDesktop!['label'].toString()
|
|
848
890
|
: 'Desktop Companion',
|
|
891
|
+
_DeviceSurface.files => 'File Explorer',
|
|
849
892
|
};
|
|
850
893
|
final subtitle = switch (surface) {
|
|
851
894
|
_DeviceSurface.browser =>
|
|
@@ -887,6 +930,8 @@ class _DeviceSurfaceHeader extends StatelessWidget {
|
|
|
887
930
|
? 'One desktop companion is online. Open the surface to fetch the latest frame.'
|
|
888
931
|
: 'No desktop companion is online. Enable Companion Mode on a signed-in desktop app.'
|
|
889
932
|
: '${selectedDesktop['platform'] ?? 'desktop'} · ${selectedDesktop['hostname'] ?? 'unknown host'}',
|
|
933
|
+
_DeviceSurface.files =>
|
|
934
|
+
'Secure per-user workspace inside the isolated VM.',
|
|
890
935
|
};
|
|
891
936
|
final statusLabel = surface == _DeviceSurface.browser
|
|
892
937
|
? browserExtensionPreferred && selectedExtension == null
|
|
@@ -904,6 +949,8 @@ class _DeviceSurfaceHeader extends StatelessWidget {
|
|
|
904
949
|
: (selectedDesktop['online'] == true
|
|
905
950
|
? 'Live'
|
|
906
951
|
: 'Offline'))
|
|
952
|
+
: surface == _DeviceSurface.files
|
|
953
|
+
? 'Isolated'
|
|
907
954
|
: (androidOnline
|
|
908
955
|
? 'Live'
|
|
909
956
|
: androidStarting
|
|
@@ -915,6 +962,8 @@ class _DeviceSurfaceHeader extends StatelessWidget {
|
|
|
915
962
|
? (selectedDesktop?['paused'] == true
|
|
916
963
|
? _warning
|
|
917
964
|
: (selectedDesktop?['online'] == true ? _success : _warning))
|
|
965
|
+
: surface == _DeviceSurface.files
|
|
966
|
+
? _success
|
|
918
967
|
: (androidOnline
|
|
919
968
|
? _success
|
|
920
969
|
: (androidStarting ? _accent : _warning));
|
|
@@ -1021,16 +1070,19 @@ class _DeviceLaunchBar extends StatelessWidget {
|
|
|
1021
1070
|
_DeviceSurface.android => _packageOrUrlHint,
|
|
1022
1071
|
_DeviceSurface.desktop =>
|
|
1023
1072
|
'Launch an app on the selected desktop (optional)',
|
|
1073
|
+
_DeviceSurface.files => 'Workspace path',
|
|
1024
1074
|
};
|
|
1025
1075
|
final buttonLabel = switch (surface) {
|
|
1026
1076
|
_DeviceSurface.browser => 'Open',
|
|
1027
1077
|
_DeviceSurface.android => starting ? 'Starting...' : 'Launch',
|
|
1028
1078
|
_DeviceSurface.desktop => 'Refresh',
|
|
1079
|
+
_DeviceSurface.files => 'Refresh',
|
|
1029
1080
|
};
|
|
1030
1081
|
final sleepLabel = switch (surface) {
|
|
1031
1082
|
_DeviceSurface.browser => 'Sleep Browser',
|
|
1032
1083
|
_DeviceSurface.android => 'Sleep Phone',
|
|
1033
1084
|
_DeviceSurface.desktop => 'Pause Desktop',
|
|
1085
|
+
_DeviceSurface.files => 'Close',
|
|
1034
1086
|
};
|
|
1035
1087
|
final narrow = MediaQuery.sizeOf(context).width < 720;
|
|
1036
1088
|
|
|
@@ -1044,6 +1096,8 @@ class _DeviceLaunchBar extends StatelessWidget {
|
|
|
1044
1096
|
? Icons.travel_explore
|
|
1045
1097
|
: surface == _DeviceSurface.desktop
|
|
1046
1098
|
? Icons.apps_outlined
|
|
1099
|
+
: surface == _DeviceSurface.files
|
|
1100
|
+
? Icons.folder_outlined
|
|
1047
1101
|
: Icons.open_in_new,
|
|
1048
1102
|
),
|
|
1049
1103
|
),
|
|
@@ -1056,6 +1110,8 @@ class _DeviceLaunchBar extends StatelessWidget {
|
|
|
1056
1110
|
? Icons.arrow_forward
|
|
1057
1111
|
: surface == _DeviceSurface.desktop
|
|
1058
1112
|
? Icons.desktop_windows_outlined
|
|
1113
|
+
: surface == _DeviceSurface.files
|
|
1114
|
+
? Icons.refresh_rounded
|
|
1059
1115
|
: Icons.play_arrow,
|
|
1060
1116
|
),
|
|
1061
1117
|
label: Text(buttonLabel),
|
|
@@ -1113,6 +1169,7 @@ class _DeviceTypeDock extends StatelessWidget {
|
|
|
1113
1169
|
_DeviceSurface.browser => 'Type into the currently focused field',
|
|
1114
1170
|
_DeviceSurface.android => 'Type into the current phone field',
|
|
1115
1171
|
_DeviceSurface.desktop => 'Type into the focused desktop field',
|
|
1172
|
+
_DeviceSurface.files => 'Edit the selected file',
|
|
1116
1173
|
};
|
|
1117
1174
|
final narrow = MediaQuery.sizeOf(context).width < 720;
|
|
1118
1175
|
|
|
@@ -1186,6 +1243,9 @@ class _DeviceQuickActions extends StatelessWidget {
|
|
|
1186
1243
|
_DeviceSurface.android => const <MapEntry<String, IconData>>[
|
|
1187
1244
|
MapEntry<String, IconData>('surface_refresh', Icons.refresh_rounded),
|
|
1188
1245
|
],
|
|
1246
|
+
_DeviceSurface.files => const <MapEntry<String, IconData>>[
|
|
1247
|
+
MapEntry<String, IconData>('surface_refresh', Icons.refresh_rounded),
|
|
1248
|
+
],
|
|
1189
1249
|
};
|
|
1190
1250
|
|
|
1191
1251
|
return Wrap(
|
|
@@ -1264,6 +1324,77 @@ class _AndroidNavDock extends StatelessWidget {
|
|
|
1264
1324
|
}
|
|
1265
1325
|
}
|
|
1266
1326
|
|
|
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,
|
|
1351
|
+
});
|
|
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
|
+
|
|
1267
1398
|
class _SurfaceSwitcher extends StatelessWidget {
|
|
1268
1399
|
const _SurfaceSwitcher({required this.surface, required this.onSelect});
|
|
1269
1400
|
|
|
@@ -1466,6 +1597,42 @@ class _InteractiveSurfacePreviewState
|
|
|
1466
1597
|
} catch (_) {}
|
|
1467
1598
|
}
|
|
1468
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
|
+
|
|
1469
1636
|
void _detachImageListener() {
|
|
1470
1637
|
if (_imageStream != null && _imageListener != null) {
|
|
1471
1638
|
_imageStream!.removeListener(_imageListener!);
|
|
@@ -1571,6 +1738,7 @@ class _InteractiveSurfacePreviewState
|
|
|
1571
1738
|
_DeviceSurface.browser => 16 / 10,
|
|
1572
1739
|
_DeviceSurface.android => 10 / 16,
|
|
1573
1740
|
_DeviceSurface.desktop => 16 / 10,
|
|
1741
|
+
_DeviceSurface.files => 16 / 10,
|
|
1574
1742
|
};
|
|
1575
1743
|
|
|
1576
1744
|
return Container(
|
|
@@ -1610,6 +1778,7 @@ class _InteractiveSurfacePreviewState
|
|
|
1610
1778
|
streamDeviceId != null &&
|
|
1611
1779
|
streamDeviceId.isNotEmpty &&
|
|
1612
1780
|
streamKey != _streamFailedKey) {
|
|
1781
|
+
final activeStreamKey = streamKey!;
|
|
1613
1782
|
return Stack(
|
|
1614
1783
|
fit: StackFit.expand,
|
|
1615
1784
|
children: <Widget>[
|
|
@@ -1619,6 +1788,10 @@ class _InteractiveSurfacePreviewState
|
|
|
1619
1788
|
deviceId: streamDeviceId,
|
|
1620
1789
|
platform: streamPlatform,
|
|
1621
1790
|
remoteResolution: _pixelSize,
|
|
1791
|
+
onFirstFrame: () =>
|
|
1792
|
+
_handleStreamFirstFrame(activeStreamKey),
|
|
1793
|
+
onFrameTimeout: () =>
|
|
1794
|
+
_handleStreamFrameTimeout(activeStreamKey),
|
|
1622
1795
|
onTap: widget.busy
|
|
1623
1796
|
? null
|
|
1624
1797
|
: (x, y) => unawaited(
|
|
@@ -1636,6 +1809,14 @@ class _InteractiveSurfacePreviewState
|
|
|
1636
1809
|
? null
|
|
1637
1810
|
: (x, y) => widget.onHover?.call(Offset(x, y)),
|
|
1638
1811
|
),
|
|
1812
|
+
const Positioned(
|
|
1813
|
+
top: 8,
|
|
1814
|
+
right: 8,
|
|
1815
|
+
child: Opacity(
|
|
1816
|
+
opacity: 0.45,
|
|
1817
|
+
child: _MutedBadge(),
|
|
1818
|
+
),
|
|
1819
|
+
),
|
|
1639
1820
|
Positioned(
|
|
1640
1821
|
left: 12,
|
|
1641
1822
|
right: 12,
|
|
@@ -1743,6 +1924,14 @@ class _InteractiveSurfacePreviewState
|
|
|
1743
1924
|
fit: BoxFit.contain,
|
|
1744
1925
|
gaplessPlayback: true,
|
|
1745
1926
|
),
|
|
1927
|
+
const Positioned(
|
|
1928
|
+
top: 8,
|
|
1929
|
+
right: 8,
|
|
1930
|
+
child: Opacity(
|
|
1931
|
+
opacity: 0.45,
|
|
1932
|
+
child: _MutedBadge(),
|
|
1933
|
+
),
|
|
1934
|
+
),
|
|
1746
1935
|
Positioned(
|
|
1747
1936
|
left: 12,
|
|
1748
1937
|
right: 12,
|
|
@@ -1815,6 +2004,7 @@ class _EmptySurfaceState extends StatelessWidget {
|
|
|
1815
2004
|
connectRequired
|
|
1816
2005
|
? 'Select Desktop'
|
|
1817
2006
|
: (busy ? 'Refreshing Desktop...' : 'Refresh Desktop'),
|
|
2007
|
+
_DeviceSurface.files => busy ? 'Loading Files...' : 'Refresh Files',
|
|
1818
2008
|
};
|
|
1819
2009
|
final message = switch ((surface, busy, isLoadingPreview)) {
|
|
1820
2010
|
(_DeviceSurface.browser, true, _) =>
|
|
@@ -1829,6 +2019,8 @@ class _EmptySurfaceState extends StatelessWidget {
|
|
|
1829
2019
|
'Waking the phone and downloading the first preview. This can take a little while.',
|
|
1830
2020
|
(_DeviceSurface.android, false, true) =>
|
|
1831
2021
|
'Downloading the latest phone preview...',
|
|
2022
|
+
(_DeviceSurface.files, true, _) => 'Loading workspace files...',
|
|
2023
|
+
(_DeviceSurface.files, false, true) => 'Loading workspace files...',
|
|
1832
2024
|
_ =>
|
|
1833
2025
|
surface == _DeviceSurface.browser
|
|
1834
2026
|
? connectRequired
|
|
@@ -2067,6 +2259,246 @@ class _ExtensionStatusBar extends StatelessWidget {
|
|
|
2067
2259
|
}
|
|
2068
2260
|
}
|
|
2069
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
|
+
|
|
2070
2502
|
class _ResultBlock extends StatelessWidget {
|
|
2071
2503
|
const _ResultBlock({required this.label, required this.value});
|
|
2072
2504
|
|
|
@@ -2091,7 +2523,7 @@ class _ResultBlock extends StatelessWidget {
|
|
|
2091
2523
|
SelectableText(
|
|
2092
2524
|
value,
|
|
2093
2525
|
style: TextStyle(
|
|
2094
|
-
fontFamily: GoogleFonts.
|
|
2526
|
+
fontFamily: GoogleFonts.geistMono().fontFamily,
|
|
2095
2527
|
fontSize: 12,
|
|
2096
2528
|
color: _textSecondary,
|
|
2097
2529
|
height: 1.5,
|