neoagent 2.3.1-beta.4 → 2.3.1-beta.41
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 +45 -0
- package/docs/capabilities.md +2 -2
- package/docs/configuration.md +12 -5
- package/docs/hardware.md +1 -1
- package/docs/integrations.md +2 -3
- package/flutter_app/.metadata +42 -0
- package/flutter_app/README.md +21 -0
- package/flutter_app/analysis_options.yaml +32 -0
- package/flutter_app/android/app/build.gradle.kts +109 -0
- package/flutter_app/android/app/src/debug/AndroidManifest.xml +7 -0
- package/flutter_app/android/app/src/main/AndroidManifest.xml +147 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +747 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthConnectGateway.kt +280 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncNotifications.kt +113 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncPayload.kt +57 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncScheduler.kt +78 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncWorker.kt +253 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/PermissionsRationaleActivity.kt +46 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +21 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +586 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +78 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +104 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiHomeWidgetProvider.kt +457 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiWidgetStore.kt +194 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/VoiceLaunchWidgetProvider.kt +67 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetConfigActivity.kt +228 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncScheduler.kt +72 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncWorker.kt +186 -0
- package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetTaskRunWorker.kt +210 -0
- package/flutter_app/android/app/src/main/res/drawable/launch_background.xml +12 -0
- package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_bg.xml +11 -0
- package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_task_bg.xml +8 -0
- package/flutter_app/android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
- package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget.xml +138 -0
- package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget_task_row.xml +52 -0
- package/flutter_app/android/app/src/main/res/layout/neoagent_voice_widget.xml +49 -0
- 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/android/app/src/main/res/values/strings.xml +12 -0
- package/flutter_app/android/app/src/main/res/values/styles.xml +18 -0
- package/flutter_app/android/app/src/main/res/values-night/styles.xml +18 -0
- package/flutter_app/android/app/src/main/res/xml/file_paths.xml +6 -0
- package/flutter_app/android/app/src/main/res/xml/neoagent_ai_widget_info.xml +12 -0
- package/flutter_app/android/app/src/main/res/xml/neoagent_voice_widget_info.xml +12 -0
- package/flutter_app/android/app/src/profile/AndroidManifest.xml +7 -0
- package/flutter_app/android/build.gradle.kts +24 -0
- package/flutter_app/android/ci-release.keystore +0 -0
- package/flutter_app/android/gradle/wrapper/gradle-wrapper.properties +5 -0
- package/flutter_app/android/gradle.properties +3 -0
- package/flutter_app/android/key.properties +4 -0
- package/flutter_app/android/settings.gradle.kts +26 -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/tray_icon_template.png +0 -0
- package/flutter_app/lib/features/location/location_service.dart +119 -0
- package/flutter_app/lib/features/notifications/notification_interceptor.dart +97 -0
- package/flutter_app/lib/main.dart +99 -0
- package/flutter_app/lib/main_account_settings.dart +1250 -0
- package/flutter_app/lib/main_admin.dart +886 -0
- package/flutter_app/lib/main_app_shell.dart +1682 -0
- package/flutter_app/lib/main_chat.dart +3352 -0
- package/flutter_app/lib/main_controller.dart +6781 -0
- package/flutter_app/lib/main_devices.dart +2301 -0
- package/flutter_app/lib/main_integrations.dart +1129 -0
- package/flutter_app/lib/main_launcher.dart +959 -0
- package/flutter_app/lib/main_launcher_entry.dart +5 -0
- package/flutter_app/lib/main_models.dart +3546 -0
- package/flutter_app/lib/main_navigation.dart +193 -0
- package/flutter_app/lib/main_operations.dart +4851 -0
- package/flutter_app/lib/main_recordings.dart +870 -0
- package/flutter_app/lib/main_runtime.dart +806 -0
- package/flutter_app/lib/main_settings.dart +2024 -0
- package/flutter_app/lib/main_shared.dart +2861 -0
- package/flutter_app/lib/main_theme.dart +204 -0
- package/flutter_app/lib/main_voice_assistant.dart +957 -0
- package/flutter_app/lib/src/android_apk_drop_zone.dart +32 -0
- package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +16 -0
- package/flutter_app/lib/src/android_apk_drop_zone_web.dart +348 -0
- package/flutter_app/lib/src/android_app_installer.dart +22 -0
- package/flutter_app/lib/src/android_app_installer_io.dart +122 -0
- package/flutter_app/lib/src/android_app_installer_stub.dart +21 -0
- package/flutter_app/lib/src/android_launcher_bridge.dart +239 -0
- package/flutter_app/lib/src/app_launch_bridge.dart +29 -0
- package/flutter_app/lib/src/app_release_updater.dart +511 -0
- package/flutter_app/lib/src/backend_client.dart +1833 -0
- package/flutter_app/lib/src/desktop_companion.dart +2 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +586 -0
- package/flutter_app/lib/src/desktop_companion_io.dart +538 -0
- package/flutter_app/lib/src/desktop_companion_stub.dart +59 -0
- package/flutter_app/lib/src/desktop_native_bridge.dart +91 -0
- package/flutter_app/lib/src/desktop_screen_capture.dart +21 -0
- package/flutter_app/lib/src/desktop_screen_capture_io.dart +142 -0
- package/flutter_app/lib/src/desktop_screen_capture_stub.dart +12 -0
- package/flutter_app/lib/src/diagnostics_logger.dart +119 -0
- package/flutter_app/lib/src/health_bridge.dart +136 -0
- package/flutter_app/lib/src/live_voice_capture.dart +85 -0
- package/flutter_app/lib/src/messaging_access_summary.dart +46 -0
- package/flutter_app/lib/src/network/app_http_client.dart +53 -0
- package/flutter_app/lib/src/network/app_http_client_factory.dart +6 -0
- package/flutter_app/lib/src/network/app_http_client_io.dart +138 -0
- package/flutter_app/lib/src/network/app_http_client_stub.dart +3 -0
- package/flutter_app/lib/src/network/app_http_client_web.dart +94 -0
- package/flutter_app/lib/src/oauth_launcher.dart +33 -0
- package/flutter_app/lib/src/oauth_launcher_io.dart +77 -0
- package/flutter_app/lib/src/oauth_launcher_stub.dart +33 -0
- package/flutter_app/lib/src/oauth_launcher_web.dart +107 -0
- package/flutter_app/lib/src/recording_bridge.dart +232 -0
- package/flutter_app/lib/src/recording_bridge_io.dart +1019 -0
- package/flutter_app/lib/src/recording_bridge_stub.dart +120 -0
- package/flutter_app/lib/src/recording_bridge_web.dart +689 -0
- package/flutter_app/lib/src/recording_payloads.dart +86 -0
- package/flutter_app/lib/src/theme/palette.dart +81 -0
- package/flutter_app/lib/src/widget_bridge.dart +49 -0
- package/flutter_app/linux/CMakeLists.txt +128 -0
- package/flutter_app/linux/flutter/CMakeLists.txt +88 -0
- package/flutter_app/linux/flutter/generated_plugin_registrant.cc +43 -0
- package/flutter_app/linux/flutter/generated_plugin_registrant.h +15 -0
- package/flutter_app/linux/flutter/generated_plugins.cmake +31 -0
- package/flutter_app/linux/runner/CMakeLists.txt +26 -0
- package/flutter_app/linux/runner/main.cc +6 -0
- package/flutter_app/linux/runner/my_application.cc +144 -0
- package/flutter_app/linux/runner/my_application.h +18 -0
- package/flutter_app/linux/runner/resources/app_icon.png +0 -0
- package/flutter_app/macos/Flutter/Flutter-Debug.xcconfig +2 -0
- package/flutter_app/macos/Flutter/Flutter-Release.xcconfig +2 -0
- package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +40 -0
- package/flutter_app/macos/Podfile +42 -0
- package/flutter_app/macos/Podfile.lock +87 -0
- package/flutter_app/macos/Runner/AppDelegate.swift +576 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -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/macos/Runner/Base.lproj/MainMenu.xib +342 -0
- package/flutter_app/macos/Runner/Configs/AppInfo.xcconfig +14 -0
- package/flutter_app/macos/Runner/Configs/Debug.xcconfig +2 -0
- package/flutter_app/macos/Runner/Configs/Release.xcconfig +2 -0
- package/flutter_app/macos/Runner/Configs/Warnings.xcconfig +13 -0
- package/flutter_app/macos/Runner/DebugProfile.entitlements +16 -0
- package/flutter_app/macos/Runner/Info.plist +36 -0
- package/flutter_app/macos/Runner/MainFlutterWindow.swift +19 -0
- package/flutter_app/macos/Runner/Release.entitlements +12 -0
- package/flutter_app/macos/Runner.xcodeproj/project.pbxproj +801 -0
- package/flutter_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/flutter_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +99 -0
- package/flutter_app/macos/Runner.xcworkspace/contents.xcworkspacedata +10 -0
- package/flutter_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
- package/flutter_app/macos/RunnerTests/RunnerTests.swift +12 -0
- package/flutter_app/patch_strings.py +12 -0
- package/flutter_app/pubspec.lock +1088 -0
- package/flutter_app/pubspec.yaml +53 -0
- package/flutter_app/test/messaging_access_summary_test.dart +22 -0
- package/flutter_app/test/recording_payloads_test.dart +53 -0
- package/flutter_app/third_party/desktop_audio_capture/LICENSE +21 -0
- package/flutter_app/third_party/desktop_audio_capture/README.md +262 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +65 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +153 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +110 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +461 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +91 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +106 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +219 -0
- package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +336 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +101 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +692 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +35 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +36 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +32 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +32 -0
- package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +878 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +27 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +1172 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +655 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +12 -0
- package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +30 -0
- package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +87 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +105 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +80 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +31 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +12 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +23 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +25 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +1117 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +115 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +777 -0
- package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +87 -0
- package/flutter_app/third_party/flutter_secure_storage_linux/linux/CMakeLists.txt +30 -0
- package/flutter_app/third_party/flutter_secure_storage_linux/linux/flutter_secure_storage_linux_plugin.cc +215 -0
- package/flutter_app/third_party/flutter_secure_storage_linux/linux/include/flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h +27 -0
- package/flutter_app/third_party/flutter_secure_storage_linux/pubspec.yaml +20 -0
- package/flutter_app/tool/generate_desktop_branding.py +219 -0
- package/flutter_app/web/favicon.png +0 -0
- package/flutter_app/web/favicon.svg +12 -0
- package/flutter_app/web/icons/Icon-192.png +0 -0
- package/flutter_app/web/icons/Icon-512.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
- package/flutter_app/web/index.html +39 -0
- package/flutter_app/web/manifest.json +35 -0
- package/flutter_app/windows/CMakeLists.txt +108 -0
- package/flutter_app/windows/flutter/CMakeLists.txt +109 -0
- package/flutter_app/windows/flutter/generated_plugin_registrant.cc +47 -0
- package/flutter_app/windows/flutter/generated_plugin_registrant.h +15 -0
- package/flutter_app/windows/flutter/generated_plugins.cmake +35 -0
- package/flutter_app/windows/runner/CMakeLists.txt +41 -0
- package/flutter_app/windows/runner/Runner.rc +121 -0
- package/flutter_app/windows/runner/flutter_window.cpp +533 -0
- package/flutter_app/windows/runner/flutter_window.h +37 -0
- package/flutter_app/windows/runner/main.cpp +53 -0
- package/flutter_app/windows/runner/resource.h +16 -0
- package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
- package/flutter_app/windows/runner/runner.exe.manifest +14 -0
- package/flutter_app/windows/runner/utils.cpp +65 -0
- package/flutter_app/windows/runner/utils.h +19 -0
- package/flutter_app/windows/runner/win32_window.cpp +299 -0
- package/flutter_app/windows/runner/win32_window.h +102 -0
- package/lib/install_helpers.js +31 -0
- package/lib/manager.js +227 -6
- package/package.json +3 -1
- package/server/db/database.js +110 -0
- package/server/http/middleware.js +55 -2
- package/server/http/routes.js +1 -0
- package/server/index.js +3 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/NOTICES +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/canvaskit/wimp.wasm +0 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/main.dart.js +74324 -73132
- package/server/routes/integrations.js +108 -1
- package/server/routes/memory.js +11 -2
- package/server/{http/routes → routes}/screenHistory.js +2 -2
- package/server/routes/settings.js +75 -2
- package/server/{http/routes → routes}/triggers.js +2 -2
- package/server/routes/wearable.js +67 -0
- package/server/services/ai/models.js +30 -0
- package/server/services/ai/providers/githubCopilot.js +97 -0
- package/server/services/ai/providers/openai.js +2 -1
- package/server/services/ai/providers/openaiCodex.js +31 -0
- package/server/services/ai/settings.js +20 -0
- package/server/services/ai/toolSelector.js +14 -1
- package/server/services/ai/tools.js +77 -4
- package/server/services/desktop/screenRecorder.js +65 -9
- package/server/services/integrations/env.js +5 -0
- package/server/services/integrations/figma/provider.js +1 -0
- package/server/services/integrations/github/common.js +106 -0
- package/server/services/integrations/github/provider.js +499 -0
- package/server/services/integrations/github/repos.js +1124 -0
- package/server/services/integrations/google/provider.js +1 -0
- package/server/services/integrations/home_assistant/provider.js +325 -26
- package/server/services/integrations/manager.js +88 -12
- package/server/services/integrations/microsoft/provider.js +1 -0
- package/server/services/integrations/oauth_provider.js +25 -8
- package/server/services/integrations/provider_config_store.js +85 -0
- package/server/services/integrations/registry.js +4 -0
- package/server/services/integrations/spotify/provider.js +1 -0
- package/server/services/integrations/trello/provider.js +842 -0
- package/server/services/manager.js +46 -1
- package/server/services/mcp/client.js +120 -23
- package/server/services/memory/manager.js +39 -2
- package/server/services/messaging/access_policy.js +10 -0
- package/server/services/messaging/manager.js +49 -0
- package/server/services/messaging/meshtastic.js +260 -0
- package/server/services/messaging/meshtastic_env.js +100 -0
- package/server/services/messaging/meshtastic_protocol.js +476 -0
- package/server/services/messaging/meshtastic_tcp_transport.js +25 -0
- package/server/services/tasks/runtime.js +1 -1
- package/server/services/voice/openaiClient.js +4 -1
- package/server/services/voice/openaiSpeech.js +6 -1
- package/server/services/voice/providers.js +52 -12
- package/server/services/voice/runtimeManager.js +136 -19
- package/server/services/voice/turnRunner.js +29 -9
- package/server/services/wearable/firmware_manifest.js +370 -0
- package/server/services/wearable/gateway.js +350 -0
- package/server/services/wearable/protocol.js +45 -0
- package/server/services/wearable/service.js +244 -0
- package/server/utils/local_secrets.js +56 -0
- package/server/utils/logger.js +37 -9
|
@@ -0,0 +1,1129 @@
|
|
|
1
|
+
part of 'main.dart';
|
|
2
|
+
|
|
3
|
+
class IntegrationsPanel extends StatelessWidget {
|
|
4
|
+
const IntegrationsPanel({super.key, required this.controller});
|
|
5
|
+
|
|
6
|
+
final NeoAgentController controller;
|
|
7
|
+
|
|
8
|
+
@override
|
|
9
|
+
Widget build(BuildContext context) {
|
|
10
|
+
return Padding(
|
|
11
|
+
padding: _pagePadding(context),
|
|
12
|
+
child: Column(
|
|
13
|
+
children: <Widget>[
|
|
14
|
+
_PageTitle(
|
|
15
|
+
title: 'Integrations',
|
|
16
|
+
subtitle:
|
|
17
|
+
'Connect and manage official integrations separately from reusable skills.',
|
|
18
|
+
),
|
|
19
|
+
const SizedBox(height: 12),
|
|
20
|
+
Expanded(child: OfficialIntegrationsTab(controller: controller)),
|
|
21
|
+
],
|
|
22
|
+
),
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class OfficialIntegrationsTab extends StatelessWidget {
|
|
28
|
+
const OfficialIntegrationsTab({super.key, required this.controller});
|
|
29
|
+
|
|
30
|
+
final NeoAgentController controller;
|
|
31
|
+
|
|
32
|
+
@override
|
|
33
|
+
Widget build(BuildContext context) {
|
|
34
|
+
final visibleIntegrations = controller.officialIntegrations
|
|
35
|
+
.where(
|
|
36
|
+
(item) =>
|
|
37
|
+
item.env.configured ||
|
|
38
|
+
item.env.setupMode == 'user' ||
|
|
39
|
+
item.isConnected,
|
|
40
|
+
)
|
|
41
|
+
.toList();
|
|
42
|
+
|
|
43
|
+
if (visibleIntegrations.isEmpty) {
|
|
44
|
+
return Card(
|
|
45
|
+
child: Padding(
|
|
46
|
+
padding: EdgeInsets.all(24),
|
|
47
|
+
child: Center(
|
|
48
|
+
child: Text(
|
|
49
|
+
'No official integrations are available yet.',
|
|
50
|
+
style: TextStyle(color: _textSecondary),
|
|
51
|
+
),
|
|
52
|
+
),
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
final connectedIntegrations = visibleIntegrations
|
|
58
|
+
.where((item) => item.isConnected)
|
|
59
|
+
.toList();
|
|
60
|
+
final availableIntegrations = visibleIntegrations
|
|
61
|
+
.where(
|
|
62
|
+
(item) =>
|
|
63
|
+
!item.isConnected &&
|
|
64
|
+
(item.env.configured || item.env.setupMode == 'user'),
|
|
65
|
+
)
|
|
66
|
+
.toList();
|
|
67
|
+
|
|
68
|
+
return Card(
|
|
69
|
+
child: ListView(
|
|
70
|
+
padding: const EdgeInsets.all(16),
|
|
71
|
+
children: <Widget>[
|
|
72
|
+
if (connectedIntegrations.isNotEmpty) ...[
|
|
73
|
+
const _IntegrationSectionTitle(title: 'Connected'),
|
|
74
|
+
...connectedIntegrations.asMap().entries.map(
|
|
75
|
+
(entry) => Padding(
|
|
76
|
+
padding: EdgeInsets.only(
|
|
77
|
+
bottom: entry.key < connectedIntegrations.length - 1 ? 12 : 0,
|
|
78
|
+
),
|
|
79
|
+
child: _buildIntegrationCard(context, entry.value),
|
|
80
|
+
),
|
|
81
|
+
),
|
|
82
|
+
],
|
|
83
|
+
if (connectedIntegrations.isNotEmpty &&
|
|
84
|
+
availableIntegrations.isNotEmpty)
|
|
85
|
+
const SizedBox(height: 24),
|
|
86
|
+
if (availableIntegrations.isNotEmpty) ...[
|
|
87
|
+
const _IntegrationSectionTitle(title: 'Available'),
|
|
88
|
+
...availableIntegrations.asMap().entries.map(
|
|
89
|
+
(entry) => Padding(
|
|
90
|
+
padding: EdgeInsets.only(
|
|
91
|
+
bottom: entry.key < availableIntegrations.length - 1 ? 12 : 0,
|
|
92
|
+
),
|
|
93
|
+
child: _buildIntegrationCard(context, entry.value),
|
|
94
|
+
),
|
|
95
|
+
),
|
|
96
|
+
],
|
|
97
|
+
],
|
|
98
|
+
),
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
Widget _buildIntegrationCard(
|
|
103
|
+
BuildContext context,
|
|
104
|
+
OfficialIntegrationItem item,
|
|
105
|
+
) {
|
|
106
|
+
return Container(
|
|
107
|
+
padding: const EdgeInsets.all(16),
|
|
108
|
+
decoration: BoxDecoration(
|
|
109
|
+
color: _bgSecondary,
|
|
110
|
+
borderRadius: BorderRadius.circular(16),
|
|
111
|
+
border: Border.all(color: item.isConnected ? _accentMuted : _border),
|
|
112
|
+
),
|
|
113
|
+
child: Column(
|
|
114
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
115
|
+
children: <Widget>[
|
|
116
|
+
Row(
|
|
117
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
118
|
+
children: <Widget>[
|
|
119
|
+
_OfficialIntegrationIcon(item: item),
|
|
120
|
+
const SizedBox(width: 12),
|
|
121
|
+
Expanded(
|
|
122
|
+
child: Column(
|
|
123
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
124
|
+
children: <Widget>[
|
|
125
|
+
Row(
|
|
126
|
+
children: <Widget>[
|
|
127
|
+
Expanded(
|
|
128
|
+
child: Text(
|
|
129
|
+
item.label,
|
|
130
|
+
style: TextStyle(
|
|
131
|
+
fontSize: 18,
|
|
132
|
+
fontWeight: FontWeight.w800,
|
|
133
|
+
),
|
|
134
|
+
),
|
|
135
|
+
),
|
|
136
|
+
_StatusPill(
|
|
137
|
+
label: item.statusLabel,
|
|
138
|
+
color: item.isConnected
|
|
139
|
+
? _success
|
|
140
|
+
: item.hasExpiredAccounts
|
|
141
|
+
? _warning
|
|
142
|
+
: item.env.configured
|
|
143
|
+
? _info
|
|
144
|
+
: _warning,
|
|
145
|
+
),
|
|
146
|
+
],
|
|
147
|
+
),
|
|
148
|
+
const SizedBox(height: 6),
|
|
149
|
+
Text(
|
|
150
|
+
item.description,
|
|
151
|
+
style: TextStyle(color: _textSecondary),
|
|
152
|
+
),
|
|
153
|
+
const SizedBox(height: 10),
|
|
154
|
+
Wrap(
|
|
155
|
+
spacing: 8,
|
|
156
|
+
runSpacing: 8,
|
|
157
|
+
children: <Widget>[
|
|
158
|
+
_MetaPill(
|
|
159
|
+
label: '${item.connection.accountCount} accounts',
|
|
160
|
+
icon: Icons.alternate_email_rounded,
|
|
161
|
+
),
|
|
162
|
+
_MetaPill(
|
|
163
|
+
label: '${item.connection.appCount} apps active',
|
|
164
|
+
icon: Icons.apps_rounded,
|
|
165
|
+
),
|
|
166
|
+
_MetaPill(
|
|
167
|
+
label: '${item.availableToolCount} tools',
|
|
168
|
+
icon: Icons.build_outlined,
|
|
169
|
+
),
|
|
170
|
+
],
|
|
171
|
+
),
|
|
172
|
+
const SizedBox(height: 10),
|
|
173
|
+
Text(
|
|
174
|
+
!item.env.configured
|
|
175
|
+
? item.env.summary
|
|
176
|
+
: item.hasExpiredAccounts
|
|
177
|
+
? 'One or more accounts expired. Reconnect the affected account to restore tool access.'
|
|
178
|
+
: !item.supportsMultipleAccounts && item.isConnected
|
|
179
|
+
? 'This integration currently supports one connected account per agent. Re-open setup to replace it.'
|
|
180
|
+
: item.isConnected
|
|
181
|
+
? 'Connect as many accounts as you want. Each app can use a different account.'
|
|
182
|
+
: ((item.connectPrompt ?? '').trim().isNotEmpty
|
|
183
|
+
? item.connectPrompt!.trim()
|
|
184
|
+
: 'Connect app accounts individually so the AI can use the right account for each official integration.'),
|
|
185
|
+
style: TextStyle(color: _textSecondary),
|
|
186
|
+
),
|
|
187
|
+
],
|
|
188
|
+
),
|
|
189
|
+
),
|
|
190
|
+
],
|
|
191
|
+
),
|
|
192
|
+
const SizedBox(height: 16),
|
|
193
|
+
...item.apps.map(
|
|
194
|
+
(app) => Padding(
|
|
195
|
+
padding: const EdgeInsets.only(bottom: 12),
|
|
196
|
+
child: _OfficialIntegrationAppCard(
|
|
197
|
+
controller: controller,
|
|
198
|
+
provider: item,
|
|
199
|
+
app: app,
|
|
200
|
+
),
|
|
201
|
+
),
|
|
202
|
+
),
|
|
203
|
+
],
|
|
204
|
+
),
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
void _openOfficialIntegrationSetupDialog(
|
|
210
|
+
BuildContext context,
|
|
211
|
+
NeoAgentController controller,
|
|
212
|
+
String providerId,
|
|
213
|
+
) {
|
|
214
|
+
switch (providerId) {
|
|
215
|
+
case 'home_assistant':
|
|
216
|
+
_showHomeAssistantSetupDialog(context, controller);
|
|
217
|
+
return;
|
|
218
|
+
case 'trello':
|
|
219
|
+
_showTrelloSetupDialog(context, controller);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
Future<void> _showHomeAssistantSetupDialog(
|
|
225
|
+
BuildContext context,
|
|
226
|
+
NeoAgentController controller,
|
|
227
|
+
) async {
|
|
228
|
+
Map<String, dynamic> existing;
|
|
229
|
+
try {
|
|
230
|
+
existing = await controller.getOfficialIntegrationConfig('home_assistant');
|
|
231
|
+
} catch (error) {
|
|
232
|
+
if (context.mounted) {
|
|
233
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
234
|
+
SnackBar(content: Text(controller.errorMessage ?? error.toString())),
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
final baseUrlController = TextEditingController(
|
|
241
|
+
text: existing['baseUrl']?.toString() ?? '',
|
|
242
|
+
);
|
|
243
|
+
final clientIdController = TextEditingController(
|
|
244
|
+
text: existing['clientId']?.toString() ?? '',
|
|
245
|
+
);
|
|
246
|
+
final clientSecretController = TextEditingController();
|
|
247
|
+
final redirectUriController = TextEditingController(
|
|
248
|
+
text: existing['redirectUri']?.toString() ?? '',
|
|
249
|
+
);
|
|
250
|
+
final hasSavedSetup =
|
|
251
|
+
(existing['baseUrl']?.toString().trim().isNotEmpty ?? false) ||
|
|
252
|
+
(existing['clientId']?.toString().trim().isNotEmpty ?? false) ||
|
|
253
|
+
existing['hasClientSecret'] == true;
|
|
254
|
+
var formError = '';
|
|
255
|
+
var saving = false;
|
|
256
|
+
|
|
257
|
+
await showDialog<void>(
|
|
258
|
+
context: context,
|
|
259
|
+
builder: (dialogContext) {
|
|
260
|
+
return StatefulBuilder(
|
|
261
|
+
builder: (dialogContext, setState) {
|
|
262
|
+
return AlertDialog(
|
|
263
|
+
title: const Text('Home Assistant Setup'),
|
|
264
|
+
content: SizedBox(
|
|
265
|
+
width: 520,
|
|
266
|
+
child: Column(
|
|
267
|
+
mainAxisSize: MainAxisSize.min,
|
|
268
|
+
children: <Widget>[
|
|
269
|
+
TextField(
|
|
270
|
+
controller: baseUrlController,
|
|
271
|
+
decoration: const InputDecoration(
|
|
272
|
+
labelText: 'Base URL',
|
|
273
|
+
hintText: 'https://ha.example.com',
|
|
274
|
+
),
|
|
275
|
+
),
|
|
276
|
+
const SizedBox(height: 10),
|
|
277
|
+
TextField(
|
|
278
|
+
controller: clientIdController,
|
|
279
|
+
decoration: const InputDecoration(
|
|
280
|
+
labelText: 'OAuth Client ID',
|
|
281
|
+
),
|
|
282
|
+
),
|
|
283
|
+
const SizedBox(height: 10),
|
|
284
|
+
TextField(
|
|
285
|
+
controller: clientSecretController,
|
|
286
|
+
obscureText: true,
|
|
287
|
+
decoration: InputDecoration(
|
|
288
|
+
labelText: 'OAuth Client Secret',
|
|
289
|
+
hintText: existing['hasClientSecret'] == true
|
|
290
|
+
? 'Saved secret exists. Enter to replace it.'
|
|
291
|
+
: null,
|
|
292
|
+
),
|
|
293
|
+
),
|
|
294
|
+
const SizedBox(height: 10),
|
|
295
|
+
TextField(
|
|
296
|
+
controller: redirectUriController,
|
|
297
|
+
decoration: const InputDecoration(
|
|
298
|
+
labelText: 'Redirect URI (optional)',
|
|
299
|
+
hintText: 'Leave blank to use the default callback URL',
|
|
300
|
+
),
|
|
301
|
+
),
|
|
302
|
+
if (formError.isNotEmpty) ...<Widget>[
|
|
303
|
+
const SizedBox(height: 10),
|
|
304
|
+
Align(
|
|
305
|
+
alignment: Alignment.centerLeft,
|
|
306
|
+
child: Text(formError, style: TextStyle(color: _danger)),
|
|
307
|
+
),
|
|
308
|
+
],
|
|
309
|
+
],
|
|
310
|
+
),
|
|
311
|
+
),
|
|
312
|
+
actions: <Widget>[
|
|
313
|
+
if (hasSavedSetup)
|
|
314
|
+
TextButton(
|
|
315
|
+
onPressed: saving
|
|
316
|
+
? null
|
|
317
|
+
: () async {
|
|
318
|
+
final shouldClear =
|
|
319
|
+
await showDialog<bool>(
|
|
320
|
+
context: dialogContext,
|
|
321
|
+
builder: (context) {
|
|
322
|
+
return AlertDialog(
|
|
323
|
+
title: const Text('Clear Setup?'),
|
|
324
|
+
content: const Text(
|
|
325
|
+
'This removes your saved Home Assistant base URL and OAuth client credentials for this agent.',
|
|
326
|
+
),
|
|
327
|
+
actions: [
|
|
328
|
+
TextButton(
|
|
329
|
+
onPressed: () =>
|
|
330
|
+
Navigator.of(context).pop(false),
|
|
331
|
+
child: const Text('Cancel'),
|
|
332
|
+
),
|
|
333
|
+
FilledButton(
|
|
334
|
+
onPressed: () =>
|
|
335
|
+
Navigator.of(context).pop(true),
|
|
336
|
+
child: const Text('Clear Setup'),
|
|
337
|
+
),
|
|
338
|
+
],
|
|
339
|
+
);
|
|
340
|
+
},
|
|
341
|
+
) ??
|
|
342
|
+
false;
|
|
343
|
+
if (!shouldClear) {
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
setState(() {
|
|
347
|
+
formError = '';
|
|
348
|
+
saving = true;
|
|
349
|
+
});
|
|
350
|
+
try {
|
|
351
|
+
await controller.clearOfficialIntegrationConfig(
|
|
352
|
+
'home_assistant',
|
|
353
|
+
);
|
|
354
|
+
if (dialogContext.mounted) {
|
|
355
|
+
Navigator.of(dialogContext).pop();
|
|
356
|
+
}
|
|
357
|
+
} catch (_) {
|
|
358
|
+
setState(() {
|
|
359
|
+
formError =
|
|
360
|
+
controller.errorMessage ??
|
|
361
|
+
'Could not clear Home Assistant setup.';
|
|
362
|
+
saving = false;
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
child: const Text('Clear Setup'),
|
|
367
|
+
),
|
|
368
|
+
TextButton(
|
|
369
|
+
onPressed: saving
|
|
370
|
+
? null
|
|
371
|
+
: () => Navigator.of(dialogContext).pop(),
|
|
372
|
+
child: const Text('Cancel'),
|
|
373
|
+
),
|
|
374
|
+
FilledButton(
|
|
375
|
+
onPressed: saving
|
|
376
|
+
? null
|
|
377
|
+
: () async {
|
|
378
|
+
setState(() {
|
|
379
|
+
formError = '';
|
|
380
|
+
});
|
|
381
|
+
final baseUrl = baseUrlController.text.trim();
|
|
382
|
+
final clientId = clientIdController.text.trim();
|
|
383
|
+
final clientSecret = clientSecretController.text.trim();
|
|
384
|
+
final hasSavedSecret =
|
|
385
|
+
existing['hasClientSecret'] == true;
|
|
386
|
+
if (baseUrl.isEmpty || clientId.isEmpty) {
|
|
387
|
+
setState(() {
|
|
388
|
+
formError =
|
|
389
|
+
'Base URL and OAuth Client ID are required.';
|
|
390
|
+
});
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
if (clientSecret.isEmpty && !hasSavedSecret) {
|
|
394
|
+
setState(() {
|
|
395
|
+
formError = 'OAuth Client Secret is required.';
|
|
396
|
+
});
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
setState(() {
|
|
401
|
+
saving = true;
|
|
402
|
+
});
|
|
403
|
+
try {
|
|
404
|
+
await controller.saveOfficialIntegrationConfig(
|
|
405
|
+
'home_assistant',
|
|
406
|
+
config: <String, dynamic>{
|
|
407
|
+
'baseUrl': baseUrl,
|
|
408
|
+
'clientId': clientId,
|
|
409
|
+
if (clientSecret.isNotEmpty)
|
|
410
|
+
'clientSecret': clientSecret,
|
|
411
|
+
'redirectUri': redirectUriController.text.trim(),
|
|
412
|
+
},
|
|
413
|
+
);
|
|
414
|
+
if (dialogContext.mounted) {
|
|
415
|
+
Navigator.of(dialogContext).pop();
|
|
416
|
+
}
|
|
417
|
+
} catch (_) {
|
|
418
|
+
setState(() {
|
|
419
|
+
formError =
|
|
420
|
+
controller.errorMessage ??
|
|
421
|
+
'Could not save Home Assistant setup.';
|
|
422
|
+
saving = false;
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
},
|
|
426
|
+
child: Text(saving ? 'Saving...' : 'Save Setup'),
|
|
427
|
+
),
|
|
428
|
+
],
|
|
429
|
+
);
|
|
430
|
+
},
|
|
431
|
+
);
|
|
432
|
+
},
|
|
433
|
+
);
|
|
434
|
+
|
|
435
|
+
baseUrlController.dispose();
|
|
436
|
+
clientIdController.dispose();
|
|
437
|
+
clientSecretController.dispose();
|
|
438
|
+
redirectUriController.dispose();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
Future<void> _showTrelloSetupDialog(
|
|
442
|
+
BuildContext context,
|
|
443
|
+
NeoAgentController controller,
|
|
444
|
+
) async {
|
|
445
|
+
Map<String, dynamic> existing;
|
|
446
|
+
try {
|
|
447
|
+
existing = await controller.getOfficialIntegrationConfig('trello');
|
|
448
|
+
} catch (error) {
|
|
449
|
+
if (context.mounted) {
|
|
450
|
+
ScaffoldMessenger.of(context).showSnackBar(
|
|
451
|
+
SnackBar(content: Text(controller.errorMessage ?? error.toString())),
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
final apiKeyConfigured = existing['apiKeyConfigured'] == true;
|
|
458
|
+
final savedApiKey = existing['apiKey']?.toString() ?? '';
|
|
459
|
+
final apiKeyManagedByServer = apiKeyConfigured && savedApiKey.trim().isEmpty;
|
|
460
|
+
final authorizeUrl = existing['authorizeUrl']?.toString() ?? '';
|
|
461
|
+
final accountCount = (existing['accountCount'] as num?)?.toInt() ?? 0;
|
|
462
|
+
final hasConnectedAccount =
|
|
463
|
+
existing['hasConnectedAccount'] == true || accountCount > 0;
|
|
464
|
+
var formError = '';
|
|
465
|
+
var connecting = false;
|
|
466
|
+
|
|
467
|
+
final apiKeyController = TextEditingController(text: savedApiKey);
|
|
468
|
+
final tokenInputController = TextEditingController();
|
|
469
|
+
|
|
470
|
+
await showDialog<void>(
|
|
471
|
+
context: context,
|
|
472
|
+
barrierDismissible: false,
|
|
473
|
+
builder: (dialogContext) {
|
|
474
|
+
return StatefulBuilder(
|
|
475
|
+
builder: (dialogContext, setState) {
|
|
476
|
+
return AlertDialog(
|
|
477
|
+
title: const Text('Trello Setup'),
|
|
478
|
+
content: SizedBox(
|
|
479
|
+
width: 520,
|
|
480
|
+
child: Column(
|
|
481
|
+
mainAxisSize: MainAxisSize.min,
|
|
482
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
483
|
+
children: <Widget>[
|
|
484
|
+
Text(
|
|
485
|
+
'Save a Trello API key for this agent, then connect one Trello account securely. The account token is stored on the server and used only for this agent.',
|
|
486
|
+
style: TextStyle(color: _textSecondary),
|
|
487
|
+
),
|
|
488
|
+
const SizedBox(height: 16),
|
|
489
|
+
_TrelloStatusItem(
|
|
490
|
+
label: 'API Key',
|
|
491
|
+
status: apiKeyConfigured ? 'Configured' : 'Not configured',
|
|
492
|
+
isConnected: apiKeyConfigured,
|
|
493
|
+
),
|
|
494
|
+
const SizedBox(height: 12),
|
|
495
|
+
_TrelloStatusItem(
|
|
496
|
+
label: 'Connected Account',
|
|
497
|
+
status: hasConnectedAccount
|
|
498
|
+
? '$accountCount ${accountCount == 1 ? 'connected account' : 'connected accounts'}'
|
|
499
|
+
: 'Not connected',
|
|
500
|
+
isConnected: hasConnectedAccount,
|
|
501
|
+
),
|
|
502
|
+
if (apiKeyManagedByServer) ...<Widget>[
|
|
503
|
+
const SizedBox(height: 12),
|
|
504
|
+
Text(
|
|
505
|
+
'This agent is using a server-managed Trello API key. You only need to authorize an account token below.',
|
|
506
|
+
style: TextStyle(color: _textSecondary),
|
|
507
|
+
),
|
|
508
|
+
] else ...<Widget>[
|
|
509
|
+
const SizedBox(height: 12),
|
|
510
|
+
TextField(
|
|
511
|
+
controller: apiKeyController,
|
|
512
|
+
onChanged: (_) => setState(() {}),
|
|
513
|
+
obscureText: true,
|
|
514
|
+
decoration: const InputDecoration(
|
|
515
|
+
labelText: 'Trello API Key',
|
|
516
|
+
border: OutlineInputBorder(),
|
|
517
|
+
),
|
|
518
|
+
),
|
|
519
|
+
],
|
|
520
|
+
if (apiKeyConfigured ||
|
|
521
|
+
apiKeyController.text.trim().isNotEmpty ||
|
|
522
|
+
apiKeyManagedByServer) ...<Widget>[
|
|
523
|
+
const SizedBox(height: 12),
|
|
524
|
+
TextField(
|
|
525
|
+
controller: tokenInputController,
|
|
526
|
+
onChanged: (_) => setState(() {}),
|
|
527
|
+
obscureText: true,
|
|
528
|
+
decoration: InputDecoration(
|
|
529
|
+
labelText: hasConnectedAccount
|
|
530
|
+
? 'Paste a replacement token'
|
|
531
|
+
: 'Paste your account token',
|
|
532
|
+
border: OutlineInputBorder(),
|
|
533
|
+
),
|
|
534
|
+
),
|
|
535
|
+
],
|
|
536
|
+
if (formError.isNotEmpty) ...<Widget>[
|
|
537
|
+
const SizedBox(height: 12),
|
|
538
|
+
Container(
|
|
539
|
+
padding: const EdgeInsets.all(8),
|
|
540
|
+
decoration: BoxDecoration(
|
|
541
|
+
color: _danger.withOpacity(0.1),
|
|
542
|
+
borderRadius: BorderRadius.circular(4),
|
|
543
|
+
border: Border.all(color: _danger.withOpacity(0.3)),
|
|
544
|
+
),
|
|
545
|
+
child: Text(
|
|
546
|
+
formError,
|
|
547
|
+
style: TextStyle(color: _danger, fontSize: 12),
|
|
548
|
+
),
|
|
549
|
+
),
|
|
550
|
+
],
|
|
551
|
+
],
|
|
552
|
+
),
|
|
553
|
+
),
|
|
554
|
+
actions: <Widget>[
|
|
555
|
+
if (apiKeyConfigured || savedApiKey.trim().isNotEmpty)
|
|
556
|
+
TextButton(
|
|
557
|
+
onPressed: connecting
|
|
558
|
+
? null
|
|
559
|
+
: () async {
|
|
560
|
+
final shouldClear =
|
|
561
|
+
await showDialog<bool>(
|
|
562
|
+
context: dialogContext,
|
|
563
|
+
builder: (context) {
|
|
564
|
+
return AlertDialog(
|
|
565
|
+
title: const Text('Disconnect Trello?'),
|
|
566
|
+
content: const Text(
|
|
567
|
+
'This removes the Trello setup and connected accounts for this agent.',
|
|
568
|
+
),
|
|
569
|
+
actions: [
|
|
570
|
+
TextButton(
|
|
571
|
+
onPressed: () =>
|
|
572
|
+
Navigator.of(context).pop(false),
|
|
573
|
+
child: const Text('Cancel'),
|
|
574
|
+
),
|
|
575
|
+
FilledButton(
|
|
576
|
+
onPressed: () =>
|
|
577
|
+
Navigator.of(context).pop(true),
|
|
578
|
+
child: const Text('Disconnect'),
|
|
579
|
+
),
|
|
580
|
+
],
|
|
581
|
+
);
|
|
582
|
+
},
|
|
583
|
+
) ??
|
|
584
|
+
false;
|
|
585
|
+
if (!shouldClear) {
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
setState(() {
|
|
589
|
+
formError = '';
|
|
590
|
+
connecting = true;
|
|
591
|
+
});
|
|
592
|
+
try {
|
|
593
|
+
await controller.clearOfficialIntegrationConfig(
|
|
594
|
+
'trello',
|
|
595
|
+
);
|
|
596
|
+
if (dialogContext.mounted) {
|
|
597
|
+
Navigator.of(dialogContext).pop();
|
|
598
|
+
}
|
|
599
|
+
} catch (_) {
|
|
600
|
+
setState(() {
|
|
601
|
+
formError =
|
|
602
|
+
controller.errorMessage ??
|
|
603
|
+
'Could not disconnect Trello.';
|
|
604
|
+
connecting = false;
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
child: const Text('Disconnect'),
|
|
609
|
+
),
|
|
610
|
+
TextButton(
|
|
611
|
+
onPressed: connecting
|
|
612
|
+
? null
|
|
613
|
+
: () => Navigator.of(dialogContext).pop(),
|
|
614
|
+
child: const Text('Close'),
|
|
615
|
+
),
|
|
616
|
+
if (authorizeUrl.isNotEmpty ||
|
|
617
|
+
apiKeyManagedByServer ||
|
|
618
|
+
apiKeyController.text.trim().isNotEmpty)
|
|
619
|
+
FilledButton.icon(
|
|
620
|
+
onPressed: connecting
|
|
621
|
+
? null
|
|
622
|
+
: () async {
|
|
623
|
+
setState(() {
|
|
624
|
+
formError = '';
|
|
625
|
+
connecting = true;
|
|
626
|
+
});
|
|
627
|
+
try {
|
|
628
|
+
final effectiveApiKey = apiKeyManagedByServer
|
|
629
|
+
? ''
|
|
630
|
+
: apiKeyController.text.trim();
|
|
631
|
+
if (!apiKeyManagedByServer &&
|
|
632
|
+
effectiveApiKey.isEmpty) {
|
|
633
|
+
setState(() {
|
|
634
|
+
formError = 'Trello API Key is required.';
|
|
635
|
+
connecting = false;
|
|
636
|
+
});
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
final url = authorizeUrl.isNotEmpty
|
|
640
|
+
? authorizeUrl
|
|
641
|
+
: 'https://trello.com/1/authorize?expiration=never&scope=read,write,account&response_type=token&key=' +
|
|
642
|
+
Uri.encodeComponent(effectiveApiKey);
|
|
643
|
+
final result = await controller._oauthLauncher
|
|
644
|
+
.openExternal(url: url, label: 'Trello');
|
|
645
|
+
if (!result.launched) {
|
|
646
|
+
setState(() {
|
|
647
|
+
formError =
|
|
648
|
+
result.error ??
|
|
649
|
+
'Could not open Trello in your browser.';
|
|
650
|
+
connecting = false;
|
|
651
|
+
});
|
|
652
|
+
} else {
|
|
653
|
+
setState(() {
|
|
654
|
+
connecting = false;
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
} catch (error) {
|
|
658
|
+
setState(() {
|
|
659
|
+
formError = error.toString();
|
|
660
|
+
connecting = false;
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
},
|
|
664
|
+
icon: const Icon(Icons.open_in_browser_rounded),
|
|
665
|
+
label: Text(connecting ? 'Opening...' : 'Open Trello'),
|
|
666
|
+
),
|
|
667
|
+
FilledButton(
|
|
668
|
+
onPressed: connecting
|
|
669
|
+
? null
|
|
670
|
+
: () async {
|
|
671
|
+
setState(() {
|
|
672
|
+
formError = '';
|
|
673
|
+
connecting = true;
|
|
674
|
+
});
|
|
675
|
+
try {
|
|
676
|
+
final apiKey = apiKeyController.text.trim();
|
|
677
|
+
final token = tokenInputController.text.trim();
|
|
678
|
+
if (!apiKeyManagedByServer && apiKey.isEmpty) {
|
|
679
|
+
setState(() {
|
|
680
|
+
formError = 'Trello API Key is required.';
|
|
681
|
+
connecting = false;
|
|
682
|
+
});
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (token.isEmpty &&
|
|
686
|
+
apiKeyConfigured &&
|
|
687
|
+
!apiKeyManagedByServer) {
|
|
688
|
+
await controller.saveOfficialIntegrationConfig(
|
|
689
|
+
'trello',
|
|
690
|
+
config: <String, dynamic>{'apiKey': apiKey},
|
|
691
|
+
);
|
|
692
|
+
} else if (token.isEmpty && !apiKeyManagedByServer) {
|
|
693
|
+
await controller.saveOfficialIntegrationConfig(
|
|
694
|
+
'trello',
|
|
695
|
+
config: <String, dynamic>{'apiKey': apiKey},
|
|
696
|
+
);
|
|
697
|
+
} else {
|
|
698
|
+
await controller.saveOfficialIntegrationConfig(
|
|
699
|
+
'trello',
|
|
700
|
+
config: <String, dynamic>{
|
|
701
|
+
if (!apiKeyManagedByServer) 'apiKey': apiKey,
|
|
702
|
+
'token': token,
|
|
703
|
+
},
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
if (dialogContext.mounted) {
|
|
707
|
+
Navigator.of(dialogContext).pop();
|
|
708
|
+
}
|
|
709
|
+
} catch (_) {
|
|
710
|
+
setState(() {
|
|
711
|
+
formError =
|
|
712
|
+
controller.errorMessage ??
|
|
713
|
+
'Could not save Trello setup.';
|
|
714
|
+
connecting = false;
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
},
|
|
718
|
+
child: Text(
|
|
719
|
+
connecting
|
|
720
|
+
? 'Saving...'
|
|
721
|
+
: tokenInputController.text.trim().isNotEmpty
|
|
722
|
+
? hasConnectedAccount
|
|
723
|
+
? 'Replace Account'
|
|
724
|
+
: 'Connect Account'
|
|
725
|
+
: 'Save Setup',
|
|
726
|
+
),
|
|
727
|
+
),
|
|
728
|
+
],
|
|
729
|
+
);
|
|
730
|
+
},
|
|
731
|
+
);
|
|
732
|
+
},
|
|
733
|
+
);
|
|
734
|
+
|
|
735
|
+
apiKeyController.dispose();
|
|
736
|
+
tokenInputController.dispose();
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
class _TrelloStatusItem extends StatelessWidget {
|
|
740
|
+
const _TrelloStatusItem({
|
|
741
|
+
required this.label,
|
|
742
|
+
required this.status,
|
|
743
|
+
required this.isConnected,
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
final String label;
|
|
747
|
+
final String status;
|
|
748
|
+
final bool isConnected;
|
|
749
|
+
|
|
750
|
+
@override
|
|
751
|
+
Widget build(BuildContext context) {
|
|
752
|
+
return Container(
|
|
753
|
+
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
754
|
+
decoration: BoxDecoration(
|
|
755
|
+
color: _bgSecondary,
|
|
756
|
+
borderRadius: BorderRadius.circular(8),
|
|
757
|
+
border: Border.all(
|
|
758
|
+
color: isConnected ? _success.withOpacity(0.3) : _border,
|
|
759
|
+
),
|
|
760
|
+
),
|
|
761
|
+
child: Row(
|
|
762
|
+
children: <Widget>[
|
|
763
|
+
Icon(
|
|
764
|
+
isConnected ? Icons.check_circle_outlined : Icons.circle_outlined,
|
|
765
|
+
size: 18,
|
|
766
|
+
color: isConnected ? _success : _textSecondary,
|
|
767
|
+
),
|
|
768
|
+
const SizedBox(width: 8),
|
|
769
|
+
Expanded(
|
|
770
|
+
child: Column(
|
|
771
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
772
|
+
children: <Widget>[
|
|
773
|
+
Text(
|
|
774
|
+
label,
|
|
775
|
+
style: TextStyle(fontSize: 12, color: _textSecondary),
|
|
776
|
+
),
|
|
777
|
+
Text(
|
|
778
|
+
status,
|
|
779
|
+
style: TextStyle(
|
|
780
|
+
fontSize: 13,
|
|
781
|
+
fontWeight: FontWeight.w500,
|
|
782
|
+
color: isConnected ? _success : _textPrimary,
|
|
783
|
+
),
|
|
784
|
+
),
|
|
785
|
+
],
|
|
786
|
+
),
|
|
787
|
+
),
|
|
788
|
+
],
|
|
789
|
+
),
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
class _OfficialIntegrationAppCard extends StatelessWidget {
|
|
795
|
+
const _OfficialIntegrationAppCard({
|
|
796
|
+
required this.controller,
|
|
797
|
+
required this.provider,
|
|
798
|
+
required this.app,
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
final NeoAgentController controller;
|
|
802
|
+
final OfficialIntegrationItem provider;
|
|
803
|
+
final OfficialIntegrationAppItem app;
|
|
804
|
+
|
|
805
|
+
@override
|
|
806
|
+
Widget build(BuildContext context) {
|
|
807
|
+
final connectBusy = controller.isOfficialIntegrationBusy(
|
|
808
|
+
'${provider.id}:${app.id}:connect',
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
return Container(
|
|
812
|
+
padding: const EdgeInsets.all(14),
|
|
813
|
+
decoration: BoxDecoration(
|
|
814
|
+
color: _bgPrimary,
|
|
815
|
+
borderRadius: BorderRadius.circular(14),
|
|
816
|
+
border: Border.all(color: _border),
|
|
817
|
+
),
|
|
818
|
+
child: Column(
|
|
819
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
820
|
+
children: <Widget>[
|
|
821
|
+
Column(
|
|
822
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
823
|
+
children: <Widget>[
|
|
824
|
+
Row(
|
|
825
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
826
|
+
children: <Widget>[
|
|
827
|
+
Expanded(
|
|
828
|
+
child: Column(
|
|
829
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
830
|
+
children: <Widget>[
|
|
831
|
+
Row(
|
|
832
|
+
children: <Widget>[
|
|
833
|
+
Expanded(
|
|
834
|
+
child: Text(
|
|
835
|
+
app.label,
|
|
836
|
+
style: TextStyle(
|
|
837
|
+
fontSize: 16,
|
|
838
|
+
fontWeight: FontWeight.w700,
|
|
839
|
+
),
|
|
840
|
+
),
|
|
841
|
+
),
|
|
842
|
+
_StatusPill(
|
|
843
|
+
label: app.statusLabel,
|
|
844
|
+
color: app.isConnected
|
|
845
|
+
? _success
|
|
846
|
+
: app.hasExpiredAccounts
|
|
847
|
+
? _warning
|
|
848
|
+
: _textSecondary,
|
|
849
|
+
),
|
|
850
|
+
],
|
|
851
|
+
),
|
|
852
|
+
if ((app.description ?? '')
|
|
853
|
+
.trim()
|
|
854
|
+
.isNotEmpty) ...<Widget>[
|
|
855
|
+
const SizedBox(height: 4),
|
|
856
|
+
Text(
|
|
857
|
+
app.description!,
|
|
858
|
+
style: TextStyle(color: _textSecondary),
|
|
859
|
+
),
|
|
860
|
+
],
|
|
861
|
+
const SizedBox(height: 8),
|
|
862
|
+
Wrap(
|
|
863
|
+
spacing: 8,
|
|
864
|
+
runSpacing: 8,
|
|
865
|
+
children: <Widget>[
|
|
866
|
+
_MetaPill(
|
|
867
|
+
label: '${app.accounts.length} accounts',
|
|
868
|
+
icon: Icons.account_circle_outlined,
|
|
869
|
+
),
|
|
870
|
+
_MetaPill(
|
|
871
|
+
label: '${app.availableToolCount} tools',
|
|
872
|
+
icon: Icons.build_circle_outlined,
|
|
873
|
+
),
|
|
874
|
+
],
|
|
875
|
+
),
|
|
876
|
+
],
|
|
877
|
+
),
|
|
878
|
+
),
|
|
879
|
+
],
|
|
880
|
+
),
|
|
881
|
+
const SizedBox(height: 12),
|
|
882
|
+
Align(
|
|
883
|
+
alignment: Alignment.centerLeft,
|
|
884
|
+
child: _buildIntegrationActionButton(context, connectBusy),
|
|
885
|
+
),
|
|
886
|
+
],
|
|
887
|
+
),
|
|
888
|
+
const SizedBox(height: 12),
|
|
889
|
+
if (app.accounts.isEmpty)
|
|
890
|
+
Text(
|
|
891
|
+
'No accounts connected yet.',
|
|
892
|
+
style: TextStyle(color: _textSecondary),
|
|
893
|
+
)
|
|
894
|
+
else
|
|
895
|
+
Column(
|
|
896
|
+
children: app.accounts.map((account) {
|
|
897
|
+
final disconnectBusy = controller.isOfficialIntegrationBusy(
|
|
898
|
+
'${provider.id}:${account.id}:disconnect',
|
|
899
|
+
);
|
|
900
|
+
final accessBusy = controller.isOfficialIntegrationBusy(
|
|
901
|
+
'${provider.id}:${account.id}:access_mode',
|
|
902
|
+
);
|
|
903
|
+
return Container(
|
|
904
|
+
margin: const EdgeInsets.only(bottom: 8),
|
|
905
|
+
padding: const EdgeInsets.all(12),
|
|
906
|
+
decoration: BoxDecoration(
|
|
907
|
+
color: _bgSecondary,
|
|
908
|
+
borderRadius: BorderRadius.circular(12),
|
|
909
|
+
border: Border.all(
|
|
910
|
+
color: account.connected ? _accentMuted : _border,
|
|
911
|
+
),
|
|
912
|
+
),
|
|
913
|
+
child: Column(
|
|
914
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
915
|
+
children: <Widget>[
|
|
916
|
+
Text(
|
|
917
|
+
account.accountEmail ?? 'Unknown account',
|
|
918
|
+
style: TextStyle(fontWeight: FontWeight.w700),
|
|
919
|
+
),
|
|
920
|
+
const SizedBox(height: 4),
|
|
921
|
+
Text(
|
|
922
|
+
'Connection #${account.id}',
|
|
923
|
+
style: TextStyle(color: _textSecondary),
|
|
924
|
+
),
|
|
925
|
+
const SizedBox(height: 4),
|
|
926
|
+
Text(
|
|
927
|
+
'Access: ${account.accessModeLabel}',
|
|
928
|
+
style: TextStyle(color: _textSecondary),
|
|
929
|
+
),
|
|
930
|
+
const SizedBox(height: 12),
|
|
931
|
+
Wrap(
|
|
932
|
+
spacing: 8,
|
|
933
|
+
runSpacing: 8,
|
|
934
|
+
crossAxisAlignment: WrapCrossAlignment.center,
|
|
935
|
+
children: <Widget>[
|
|
936
|
+
PopupMenuButton<String>(
|
|
937
|
+
enabled: !accessBusy,
|
|
938
|
+
tooltip: 'Access mode',
|
|
939
|
+
onSelected: (value) {
|
|
940
|
+
if (value == account.accessMode) return;
|
|
941
|
+
controller.setOfficialIntegrationAccessMode(
|
|
942
|
+
provider.id,
|
|
943
|
+
connectionId: account.id,
|
|
944
|
+
accessMode: value,
|
|
945
|
+
);
|
|
946
|
+
},
|
|
947
|
+
itemBuilder: (context) =>
|
|
948
|
+
const <PopupMenuEntry<String>>[
|
|
949
|
+
PopupMenuItem<String>(
|
|
950
|
+
value: 'read_write',
|
|
951
|
+
child: Text('Read / Write'),
|
|
952
|
+
),
|
|
953
|
+
PopupMenuItem<String>(
|
|
954
|
+
value: 'read_only',
|
|
955
|
+
child: Text('Read Only'),
|
|
956
|
+
),
|
|
957
|
+
],
|
|
958
|
+
child: Container(
|
|
959
|
+
padding: const EdgeInsets.symmetric(
|
|
960
|
+
horizontal: 10,
|
|
961
|
+
vertical: 8,
|
|
962
|
+
),
|
|
963
|
+
decoration: BoxDecoration(
|
|
964
|
+
borderRadius: BorderRadius.circular(10),
|
|
965
|
+
border: Border.all(color: _border),
|
|
966
|
+
),
|
|
967
|
+
child: Row(
|
|
968
|
+
mainAxisSize: MainAxisSize.min,
|
|
969
|
+
children: <Widget>[
|
|
970
|
+
Icon(
|
|
971
|
+
Icons.lock_open_rounded,
|
|
972
|
+
size: 16,
|
|
973
|
+
color: _textSecondary,
|
|
974
|
+
),
|
|
975
|
+
const SizedBox(width: 6),
|
|
976
|
+
Text(
|
|
977
|
+
accessBusy
|
|
978
|
+
? 'Saving...'
|
|
979
|
+
: account.accessModeLabel,
|
|
980
|
+
style: TextStyle(color: _textSecondary),
|
|
981
|
+
),
|
|
982
|
+
],
|
|
983
|
+
),
|
|
984
|
+
),
|
|
985
|
+
),
|
|
986
|
+
_StatusPill(
|
|
987
|
+
label: account.statusLabel,
|
|
988
|
+
color: account.connected
|
|
989
|
+
? _success
|
|
990
|
+
: account.isExpired
|
|
991
|
+
? _warning
|
|
992
|
+
: _textSecondary,
|
|
993
|
+
),
|
|
994
|
+
OutlinedButton.icon(
|
|
995
|
+
onPressed: disconnectBusy
|
|
996
|
+
? null
|
|
997
|
+
: () =>
|
|
998
|
+
controller.disconnectOfficialIntegration(
|
|
999
|
+
provider.id,
|
|
1000
|
+
connectionId: account.id,
|
|
1001
|
+
),
|
|
1002
|
+
icon: Icon(Icons.link_off_rounded),
|
|
1003
|
+
label: Text(
|
|
1004
|
+
disconnectBusy ? 'Working...' : 'Disconnect',
|
|
1005
|
+
),
|
|
1006
|
+
),
|
|
1007
|
+
],
|
|
1008
|
+
),
|
|
1009
|
+
],
|
|
1010
|
+
),
|
|
1011
|
+
);
|
|
1012
|
+
}).toList(),
|
|
1013
|
+
),
|
|
1014
|
+
],
|
|
1015
|
+
),
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
Widget _buildIntegrationActionButton(BuildContext context, bool connectBusy) {
|
|
1020
|
+
if (provider.connectionMethod == 'user_config') {
|
|
1021
|
+
return FilledButton.icon(
|
|
1022
|
+
onPressed: () => _openOfficialIntegrationSetupDialog(
|
|
1023
|
+
context,
|
|
1024
|
+
controller,
|
|
1025
|
+
provider.id,
|
|
1026
|
+
),
|
|
1027
|
+
icon: const Icon(Icons.settings_rounded),
|
|
1028
|
+
label: Text(
|
|
1029
|
+
provider.env.configured ? 'Manage Setup' : 'Complete Setup',
|
|
1030
|
+
),
|
|
1031
|
+
);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
if (!provider.env.configured) {
|
|
1035
|
+
return provider.env.setupMode == 'user'
|
|
1036
|
+
? FilledButton.icon(
|
|
1037
|
+
onPressed: () => _openOfficialIntegrationSetupDialog(
|
|
1038
|
+
context,
|
|
1039
|
+
controller,
|
|
1040
|
+
provider.id,
|
|
1041
|
+
),
|
|
1042
|
+
icon: const Icon(Icons.settings_rounded),
|
|
1043
|
+
label: const Text('Configure'),
|
|
1044
|
+
)
|
|
1045
|
+
: OutlinedButton.icon(
|
|
1046
|
+
onPressed: null,
|
|
1047
|
+
icon: const Icon(Icons.settings_suggest_outlined),
|
|
1048
|
+
label: const Text('Admin Setup Required'),
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
return FilledButton.icon(
|
|
1053
|
+
onPressed: connectBusy
|
|
1054
|
+
? null
|
|
1055
|
+
: () => controller.connectOfficialIntegration(
|
|
1056
|
+
provider.id,
|
|
1057
|
+
appId: app.id,
|
|
1058
|
+
),
|
|
1059
|
+
icon: const Icon(Icons.link_rounded),
|
|
1060
|
+
label: Text(
|
|
1061
|
+
connectBusy
|
|
1062
|
+
? 'Connecting...'
|
|
1063
|
+
: provider.supportsMultipleAccounts && app.isConnected
|
|
1064
|
+
? 'Add Account'
|
|
1065
|
+
: 'Connect Account',
|
|
1066
|
+
),
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
class _OfficialIntegrationIcon extends StatelessWidget {
|
|
1072
|
+
const _OfficialIntegrationIcon({required this.item});
|
|
1073
|
+
|
|
1074
|
+
final OfficialIntegrationItem item;
|
|
1075
|
+
|
|
1076
|
+
@override
|
|
1077
|
+
Widget build(BuildContext context) {
|
|
1078
|
+
final color = switch (item.icon) {
|
|
1079
|
+
'google' => const Color(0xFF4285F4),
|
|
1080
|
+
'trello' => const Color(0xFF0C66E4),
|
|
1081
|
+
_ => _accent,
|
|
1082
|
+
};
|
|
1083
|
+
final label = switch (item.icon) {
|
|
1084
|
+
'google' => 'G',
|
|
1085
|
+
'trello' => 'T',
|
|
1086
|
+
_ => item.label.isNotEmpty ? item.label[0] : '?',
|
|
1087
|
+
};
|
|
1088
|
+
return Container(
|
|
1089
|
+
width: 44,
|
|
1090
|
+
height: 44,
|
|
1091
|
+
decoration: BoxDecoration(
|
|
1092
|
+
color: color.withValues(alpha: 0.18),
|
|
1093
|
+
borderRadius: BorderRadius.circular(14),
|
|
1094
|
+
border: Border.all(color: color.withValues(alpha: 0.36)),
|
|
1095
|
+
),
|
|
1096
|
+
alignment: Alignment.center,
|
|
1097
|
+
child: Text(
|
|
1098
|
+
label,
|
|
1099
|
+
style: TextStyle(
|
|
1100
|
+
color: color,
|
|
1101
|
+
fontSize: 20,
|
|
1102
|
+
fontWeight: FontWeight.w800,
|
|
1103
|
+
),
|
|
1104
|
+
),
|
|
1105
|
+
);
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
class _IntegrationSectionTitle extends StatelessWidget {
|
|
1110
|
+
const _IntegrationSectionTitle({required this.title});
|
|
1111
|
+
|
|
1112
|
+
final String title;
|
|
1113
|
+
|
|
1114
|
+
@override
|
|
1115
|
+
Widget build(BuildContext context) {
|
|
1116
|
+
return Padding(
|
|
1117
|
+
padding: const EdgeInsets.only(top: 8, bottom: 12),
|
|
1118
|
+
child: Text(
|
|
1119
|
+
title,
|
|
1120
|
+
style: TextStyle(
|
|
1121
|
+
fontSize: 14,
|
|
1122
|
+
fontWeight: FontWeight.w700,
|
|
1123
|
+
color: _textSecondary,
|
|
1124
|
+
letterSpacing: 0.5,
|
|
1125
|
+
),
|
|
1126
|
+
),
|
|
1127
|
+
);
|
|
1128
|
+
}
|
|
1129
|
+
}
|