neoagent 2.3.1-beta.2 → 2.3.1-beta.21
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 +39 -0
- package/README.md +2 -0
- package/docs/capabilities.md +2 -2
- package/docs/configuration.md +13 -5
- package/docs/integrations.md +4 -1
- 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 +23057 -0
- package/flutter_app/lib/main_app_shell.dart +1682 -0
- package/flutter_app/lib/main_integrations.dart +931 -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 +3473 -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 +831 -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/manager.js +231 -7
- package/package.json +3 -1
- package/server/db/database.js +68 -0
- package/server/http/middleware.js +50 -0
- package/server/http/routes.js +3 -1
- package/server/index.js +1 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/NOTICES +61 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +65262 -64422
- package/server/routes/integrations.js +86 -0
- package/server/routes/memory.js +11 -2
- package/server/routes/screenHistory.js +46 -0
- package/server/routes/triggers.js +81 -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/systemPrompt.js +1 -1
- package/server/services/ai/tools.js +35 -6
- package/server/services/browser/controller.js +47 -3
- package/server/services/desktop/screenRecorder.js +172 -0
- package/server/services/integrations/env.js +5 -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/home_assistant/provider.js +306 -26
- package/server/services/integrations/manager.js +63 -7
- package/server/services/integrations/oauth_provider.js +13 -6
- package/server/services/integrations/provider_config_store.js +76 -0
- package/server/services/integrations/registry.js +4 -0
- package/server/services/integrations/trello/provider.js +744 -0
- package/server/services/integrations/whatsapp/provider.js +6 -2
- package/server/services/manager.js +22 -0
- package/server/services/memory/manager.js +39 -2
- package/server/services/skills/base_catalog.js +33 -0
- package/server/services/tasks/adapters/index.js +1 -0
- package/server/services/tasks/adapters/manual.js +12 -0
- package/server/services/tasks/runtime.js +1 -1
- package/server/services/voice/openaiClient.js +4 -1
- package/server/services/voice/providers.js +2 -1
- package/server/services/widgets/service.js +49 -4
- package/server/utils/local_secrets.js +56 -0
- package/server/utils/logger.js +37 -9
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import 'dart:typed_data';
|
|
2
|
+
|
|
3
|
+
import 'package:flutter/widgets.dart';
|
|
4
|
+
|
|
5
|
+
import 'android_apk_drop_zone_stub.dart'
|
|
6
|
+
if (dart.library.html) 'android_apk_drop_zone_web.dart';
|
|
7
|
+
|
|
8
|
+
typedef AndroidApkInstallCallback =
|
|
9
|
+
Future<void> Function({required String filename, required Uint8List bytes});
|
|
10
|
+
|
|
11
|
+
class AndroidApkDropZone extends StatelessWidget {
|
|
12
|
+
const AndroidApkDropZone({
|
|
13
|
+
super.key,
|
|
14
|
+
required this.enabled,
|
|
15
|
+
required this.busy,
|
|
16
|
+
required this.onInstall,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
final bool enabled;
|
|
20
|
+
final bool busy;
|
|
21
|
+
final AndroidApkInstallCallback onInstall;
|
|
22
|
+
|
|
23
|
+
@override
|
|
24
|
+
Widget build(BuildContext context) {
|
|
25
|
+
return buildAndroidApkDropZone(
|
|
26
|
+
context,
|
|
27
|
+
enabled: enabled,
|
|
28
|
+
busy: busy,
|
|
29
|
+
onInstall: onInstall,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import 'dart:typed_data';
|
|
2
|
+
|
|
3
|
+
import 'package:flutter/widgets.dart';
|
|
4
|
+
|
|
5
|
+
Widget buildAndroidApkDropZone(
|
|
6
|
+
BuildContext context, {
|
|
7
|
+
required bool enabled,
|
|
8
|
+
required bool busy,
|
|
9
|
+
required Future<void> Function({
|
|
10
|
+
required String filename,
|
|
11
|
+
required Uint8List bytes,
|
|
12
|
+
})
|
|
13
|
+
onInstall,
|
|
14
|
+
}) {
|
|
15
|
+
return const SizedBox.shrink();
|
|
16
|
+
}
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
// ignore_for_file: avoid_web_libraries_in_flutter, deprecated_member_use
|
|
2
|
+
|
|
3
|
+
import 'dart:async';
|
|
4
|
+
import 'dart:html' as html;
|
|
5
|
+
import 'dart:typed_data';
|
|
6
|
+
import 'dart:ui_web' as ui_web;
|
|
7
|
+
|
|
8
|
+
import 'package:flutter/material.dart';
|
|
9
|
+
|
|
10
|
+
int _androidApkDropZoneViewId = 0;
|
|
11
|
+
const Set<String> _supportedAndroidInstallExtensions = <String>{
|
|
12
|
+
'.apk',
|
|
13
|
+
'.apks',
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
Widget buildAndroidApkDropZone(
|
|
17
|
+
BuildContext context, {
|
|
18
|
+
required bool enabled,
|
|
19
|
+
required bool busy,
|
|
20
|
+
required Future<void> Function({
|
|
21
|
+
required String filename,
|
|
22
|
+
required Uint8List bytes,
|
|
23
|
+
})
|
|
24
|
+
onInstall,
|
|
25
|
+
}) {
|
|
26
|
+
return _AndroidApkDropZoneWeb(
|
|
27
|
+
enabled: enabled,
|
|
28
|
+
busy: busy,
|
|
29
|
+
onInstall: onInstall,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class _AndroidApkDropZoneWeb extends StatefulWidget {
|
|
34
|
+
const _AndroidApkDropZoneWeb({
|
|
35
|
+
required this.enabled,
|
|
36
|
+
required this.busy,
|
|
37
|
+
required this.onInstall,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
final bool enabled;
|
|
41
|
+
final bool busy;
|
|
42
|
+
final Future<void> Function({
|
|
43
|
+
required String filename,
|
|
44
|
+
required Uint8List bytes,
|
|
45
|
+
})
|
|
46
|
+
onInstall;
|
|
47
|
+
|
|
48
|
+
@override
|
|
49
|
+
State<_AndroidApkDropZoneWeb> createState() => _AndroidApkDropZoneWebState();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
class _AndroidApkDropZoneWebState extends State<_AndroidApkDropZoneWeb> {
|
|
53
|
+
late final String _viewType;
|
|
54
|
+
late final html.DivElement _dropElement;
|
|
55
|
+
late final html.FileUploadInputElement _fileInput;
|
|
56
|
+
final List<StreamSubscription<dynamic>> _subscriptions =
|
|
57
|
+
<StreamSubscription<dynamic>>[];
|
|
58
|
+
bool _dragActive = false;
|
|
59
|
+
|
|
60
|
+
@override
|
|
61
|
+
void initState() {
|
|
62
|
+
super.initState();
|
|
63
|
+
_viewType = 'neoagent-android-apk-drop-zone-${_androidApkDropZoneViewId++}';
|
|
64
|
+
_dropElement = html.DivElement()
|
|
65
|
+
..setAttribute('role', 'button')
|
|
66
|
+
..setAttribute(
|
|
67
|
+
'aria-label',
|
|
68
|
+
'Drop an APK or APK bundle here to install it',
|
|
69
|
+
)
|
|
70
|
+
..tabIndex = 0
|
|
71
|
+
..style.width = '100%'
|
|
72
|
+
..style.height = '100%'
|
|
73
|
+
..style.display = 'block'
|
|
74
|
+
..style.background = 'rgba(0, 0, 0, 0.001)'
|
|
75
|
+
..style.cursor = 'pointer';
|
|
76
|
+
_fileInput = html.FileUploadInputElement()
|
|
77
|
+
..accept = '.apk,.apks'
|
|
78
|
+
..multiple = false
|
|
79
|
+
..style.display = 'none';
|
|
80
|
+
_dropElement.append(_fileInput);
|
|
81
|
+
|
|
82
|
+
_subscriptions.addAll(<StreamSubscription<dynamic>>[
|
|
83
|
+
_dropElement.onClick.listen((_) => _openPicker()),
|
|
84
|
+
_dropElement.onKeyDown.listen((event) {
|
|
85
|
+
if (event.key == 'Enter' || event.key == ' ') {
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
_openPicker();
|
|
88
|
+
}
|
|
89
|
+
}),
|
|
90
|
+
_dropElement.onDragEnter.listen((event) {
|
|
91
|
+
event.preventDefault();
|
|
92
|
+
if (!_dragActive && mounted) {
|
|
93
|
+
setState(() => _dragActive = true);
|
|
94
|
+
}
|
|
95
|
+
}),
|
|
96
|
+
_dropElement.onDragOver.listen((event) {
|
|
97
|
+
event.preventDefault();
|
|
98
|
+
event.dataTransfer.dropEffect = 'copy';
|
|
99
|
+
if (!_dragActive && mounted) {
|
|
100
|
+
setState(() => _dragActive = true);
|
|
101
|
+
}
|
|
102
|
+
}),
|
|
103
|
+
_dropElement.onDragLeave.listen((event) {
|
|
104
|
+
event.preventDefault();
|
|
105
|
+
if (_dragActive && mounted) {
|
|
106
|
+
setState(() => _dragActive = false);
|
|
107
|
+
}
|
|
108
|
+
}),
|
|
109
|
+
_dropElement.onDrop.listen((event) {
|
|
110
|
+
event.preventDefault();
|
|
111
|
+
if (_dragActive && mounted) {
|
|
112
|
+
setState(() => _dragActive = false);
|
|
113
|
+
}
|
|
114
|
+
final files = event.dataTransfer.files;
|
|
115
|
+
if (files == null || files.isEmpty) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
unawaited(_handleFile(files.first));
|
|
119
|
+
}),
|
|
120
|
+
_fileInput.onChange.listen((_) {
|
|
121
|
+
final files = _fileInput.files;
|
|
122
|
+
if (files == null || files.isEmpty) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
unawaited(_handleFile(files.first));
|
|
126
|
+
}),
|
|
127
|
+
]);
|
|
128
|
+
|
|
129
|
+
ui_web.platformViewRegistry.registerViewFactory(_viewType, (int _) {
|
|
130
|
+
return _dropElement;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
@override
|
|
135
|
+
void dispose() {
|
|
136
|
+
for (final subscription in _subscriptions) {
|
|
137
|
+
subscription.cancel();
|
|
138
|
+
}
|
|
139
|
+
_dropElement.remove();
|
|
140
|
+
super.dispose();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
void _openPicker() {
|
|
144
|
+
if (!widget.enabled || widget.busy) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
_fileInput.value = '';
|
|
148
|
+
_fileInput.click();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
Future<void> _handleFile(html.File file) async {
|
|
152
|
+
if (!widget.enabled || widget.busy) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (!_isSupportedInstallFile(file.name)) {
|
|
156
|
+
_showError('Only .apk or .apks files can be installed.');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
final bytes = await _readFileBytes(file);
|
|
161
|
+
if (!mounted) {
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
await widget.onInstall(filename: file.name, bytes: bytes);
|
|
165
|
+
} catch (error) {
|
|
166
|
+
_showError(error.toString().replaceFirst('Exception: ', ''));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
Future<Uint8List> _readFileBytes(html.File file) {
|
|
171
|
+
final completer = Completer<Uint8List>();
|
|
172
|
+
final reader = html.FileReader();
|
|
173
|
+
reader.onLoad.listen((_) {
|
|
174
|
+
final result = reader.result;
|
|
175
|
+
if (result is ByteBuffer) {
|
|
176
|
+
completer.complete(Uint8List.view(result));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (result is Uint8List) {
|
|
180
|
+
completer.complete(result);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (!completer.isCompleted) {
|
|
184
|
+
completer.completeError(
|
|
185
|
+
StateError('Could not read the Android app package.'),
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
reader.onError.listen((_) {
|
|
190
|
+
if (!completer.isCompleted) {
|
|
191
|
+
completer.completeError(
|
|
192
|
+
reader.error ?? StateError('Could not read the Android app package.'),
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
reader.readAsArrayBuffer(file);
|
|
197
|
+
return completer.future;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
void _showError(String message) {
|
|
201
|
+
if (!mounted) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
final messenger = ScaffoldMessenger.maybeOf(context);
|
|
205
|
+
if (messenger == null) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
messenger.showSnackBar(SnackBar(content: Text(message)));
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
@override
|
|
212
|
+
Widget build(BuildContext context) {
|
|
213
|
+
final theme = Theme.of(context);
|
|
214
|
+
final colorScheme = theme.colorScheme;
|
|
215
|
+
final cardColor = theme.cardTheme.color ?? colorScheme.surface;
|
|
216
|
+
final onSurface = colorScheme.onSurface;
|
|
217
|
+
final outline = colorScheme.outlineVariant;
|
|
218
|
+
final primary = colorScheme.primary;
|
|
219
|
+
final activeBorderColor = _dragActive ? primary : outline;
|
|
220
|
+
final background = _dragActive
|
|
221
|
+
? primary.withValues(
|
|
222
|
+
alpha: theme.brightness == Brightness.dark ? 0.14 : 0.10,
|
|
223
|
+
)
|
|
224
|
+
: widget.enabled
|
|
225
|
+
? cardColor
|
|
226
|
+
: colorScheme.surfaceContainerHighest.withValues(
|
|
227
|
+
alpha: theme.brightness == Brightness.dark ? 0.84 : 0.92,
|
|
228
|
+
);
|
|
229
|
+
final iconBackground = _dragActive
|
|
230
|
+
? primary.withValues(
|
|
231
|
+
alpha: theme.brightness == Brightness.dark ? 0.18 : 0.14,
|
|
232
|
+
)
|
|
233
|
+
: colorScheme.surfaceContainerHighest.withValues(
|
|
234
|
+
alpha: theme.brightness == Brightness.dark ? 0.72 : 0.92,
|
|
235
|
+
);
|
|
236
|
+
final titleColor = onSurface;
|
|
237
|
+
final subtitleColor = colorScheme.onSurfaceVariant;
|
|
238
|
+
final dropZoneBorderColor = _dragActive ? primary : outline;
|
|
239
|
+
final dropZoneBackground = _dragActive
|
|
240
|
+
? primary.withValues(
|
|
241
|
+
alpha: theme.brightness == Brightness.dark ? 0.10 : 0.08,
|
|
242
|
+
)
|
|
243
|
+
: colorScheme.surface.withValues(
|
|
244
|
+
alpha: theme.brightness == Brightness.dark ? 0.18 : 0.72,
|
|
245
|
+
);
|
|
246
|
+
final subtitle = widget.busy
|
|
247
|
+
? 'Installing app package on the phone...'
|
|
248
|
+
: widget.enabled
|
|
249
|
+
? 'Drag and drop a .apk or .apks file here, or click to browse.'
|
|
250
|
+
: 'Start the Android phone first, then drop a .apk or .apks file here.';
|
|
251
|
+
|
|
252
|
+
return Container(
|
|
253
|
+
width: double.infinity,
|
|
254
|
+
padding: const EdgeInsets.all(14),
|
|
255
|
+
decoration: BoxDecoration(
|
|
256
|
+
color: background,
|
|
257
|
+
borderRadius: BorderRadius.circular(18),
|
|
258
|
+
border: Border.all(color: activeBorderColor),
|
|
259
|
+
),
|
|
260
|
+
child: Column(
|
|
261
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
262
|
+
children: <Widget>[
|
|
263
|
+
Row(
|
|
264
|
+
children: <Widget>[
|
|
265
|
+
Container(
|
|
266
|
+
width: 38,
|
|
267
|
+
height: 38,
|
|
268
|
+
decoration: BoxDecoration(
|
|
269
|
+
color: iconBackground,
|
|
270
|
+
borderRadius: BorderRadius.circular(12),
|
|
271
|
+
),
|
|
272
|
+
child: Icon(
|
|
273
|
+
Icons.install_mobile_outlined,
|
|
274
|
+
color: _dragActive ? primary : titleColor,
|
|
275
|
+
),
|
|
276
|
+
),
|
|
277
|
+
const SizedBox(width: 12),
|
|
278
|
+
Expanded(
|
|
279
|
+
child: Column(
|
|
280
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
281
|
+
children: <Widget>[
|
|
282
|
+
Text(
|
|
283
|
+
'Install APK / Bundle',
|
|
284
|
+
style: theme.textTheme.titleMedium?.copyWith(
|
|
285
|
+
color: titleColor,
|
|
286
|
+
fontWeight: FontWeight.w700,
|
|
287
|
+
),
|
|
288
|
+
),
|
|
289
|
+
const SizedBox(height: 4),
|
|
290
|
+
Text(
|
|
291
|
+
subtitle,
|
|
292
|
+
style: theme.textTheme.bodyMedium?.copyWith(
|
|
293
|
+
color: subtitleColor,
|
|
294
|
+
height: 1.4,
|
|
295
|
+
),
|
|
296
|
+
),
|
|
297
|
+
],
|
|
298
|
+
),
|
|
299
|
+
),
|
|
300
|
+
],
|
|
301
|
+
),
|
|
302
|
+
const SizedBox(height: 12),
|
|
303
|
+
SizedBox(
|
|
304
|
+
height: 92,
|
|
305
|
+
child: Stack(
|
|
306
|
+
children: <Widget>[
|
|
307
|
+
Positioned.fill(
|
|
308
|
+
child: DecoratedBox(
|
|
309
|
+
decoration: BoxDecoration(
|
|
310
|
+
borderRadius: BorderRadius.circular(14),
|
|
311
|
+
border: Border.all(color: dropZoneBorderColor),
|
|
312
|
+
color: dropZoneBackground,
|
|
313
|
+
),
|
|
314
|
+
child: Center(
|
|
315
|
+
child: Text(
|
|
316
|
+
widget.busy
|
|
317
|
+
? 'Installing...'
|
|
318
|
+
: _dragActive
|
|
319
|
+
? 'Release to install this package'
|
|
320
|
+
: 'Drop APK or .apks Here',
|
|
321
|
+
style: theme.textTheme.titleSmall?.copyWith(
|
|
322
|
+
color: titleColor,
|
|
323
|
+
fontWeight: FontWeight.w700,
|
|
324
|
+
letterSpacing: 0.2,
|
|
325
|
+
),
|
|
326
|
+
),
|
|
327
|
+
),
|
|
328
|
+
),
|
|
329
|
+
),
|
|
330
|
+
Positioned.fill(
|
|
331
|
+
child: ClipRRect(
|
|
332
|
+
borderRadius: BorderRadius.circular(14),
|
|
333
|
+
child: HtmlElementView(viewType: _viewType),
|
|
334
|
+
),
|
|
335
|
+
),
|
|
336
|
+
],
|
|
337
|
+
),
|
|
338
|
+
),
|
|
339
|
+
],
|
|
340
|
+
),
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
bool _isSupportedInstallFile(String filename) {
|
|
346
|
+
final normalized = filename.toLowerCase();
|
|
347
|
+
return _supportedAndroidInstallExtensions.any(normalized.endsWith);
|
|
348
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import 'android_app_installer_stub.dart'
|
|
2
|
+
if (dart.library.io) 'android_app_installer_io.dart';
|
|
3
|
+
|
|
4
|
+
AndroidAppInstaller createAndroidAppInstaller() =>
|
|
5
|
+
createPlatformAndroidAppInstaller();
|
|
6
|
+
|
|
7
|
+
abstract class AndroidAppInstaller {
|
|
8
|
+
bool get supported;
|
|
9
|
+
|
|
10
|
+
Future<AndroidAppInstallResult> installApkFromUrl({
|
|
11
|
+
required String downloadUrl,
|
|
12
|
+
required String fileName,
|
|
13
|
+
Map<String, String> headers = const <String, String>{},
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
class AndroidAppInstallResult {
|
|
18
|
+
const AndroidAppInstallResult({required this.launched, this.error});
|
|
19
|
+
|
|
20
|
+
final bool launched;
|
|
21
|
+
final String? error;
|
|
22
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import 'dart:io';
|
|
2
|
+
|
|
3
|
+
import 'package:flutter/foundation.dart';
|
|
4
|
+
import 'package:flutter/services.dart';
|
|
5
|
+
import 'package:http/http.dart' as http;
|
|
6
|
+
import 'package:path_provider/path_provider.dart';
|
|
7
|
+
|
|
8
|
+
import 'android_app_installer.dart';
|
|
9
|
+
|
|
10
|
+
const MethodChannel _androidAppInstallerChannel = MethodChannel(
|
|
11
|
+
'neoagent/app_update',
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
AndroidAppInstaller createPlatformAndroidAppInstaller() =>
|
|
15
|
+
_IoAndroidAppInstaller();
|
|
16
|
+
|
|
17
|
+
class _IoAndroidAppInstaller implements AndroidAppInstaller {
|
|
18
|
+
_IoAndroidAppInstaller({http.Client? client})
|
|
19
|
+
: _client = client ?? http.Client();
|
|
20
|
+
|
|
21
|
+
final http.Client _client;
|
|
22
|
+
|
|
23
|
+
@override
|
|
24
|
+
bool get supported =>
|
|
25
|
+
defaultTargetPlatform == TargetPlatform.android && !kIsWeb;
|
|
26
|
+
|
|
27
|
+
@override
|
|
28
|
+
Future<AndroidAppInstallResult> installApkFromUrl({
|
|
29
|
+
required String downloadUrl,
|
|
30
|
+
required String fileName,
|
|
31
|
+
Map<String, String> headers = const <String, String>{},
|
|
32
|
+
}) async {
|
|
33
|
+
if (!supported) {
|
|
34
|
+
return const AndroidAppInstallResult(
|
|
35
|
+
launched: false,
|
|
36
|
+
error: 'Android APK install is unavailable on this platform.',
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
final canInstall = await _androidAppInstallerChannel.invokeMethod<bool>(
|
|
42
|
+
'canRequestPackageInstalls',
|
|
43
|
+
);
|
|
44
|
+
if (canInstall != true) {
|
|
45
|
+
await _androidAppInstallerChannel.invokeMethod<void>(
|
|
46
|
+
'openInstallUnknownAppsSettings',
|
|
47
|
+
);
|
|
48
|
+
return const AndroidAppInstallResult(
|
|
49
|
+
launched: false,
|
|
50
|
+
error:
|
|
51
|
+
'Allow "Install unknown apps" for NeoAgent, then retry the update.',
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
final apkFile = await _downloadApk(
|
|
56
|
+
downloadUrl: downloadUrl,
|
|
57
|
+
fileName: fileName,
|
|
58
|
+
headers: headers,
|
|
59
|
+
);
|
|
60
|
+
final launched = await _androidAppInstallerChannel.invokeMethod<bool>(
|
|
61
|
+
'installApk',
|
|
62
|
+
<String, dynamic>{'apkPath': apkFile.path},
|
|
63
|
+
);
|
|
64
|
+
if (launched == true) {
|
|
65
|
+
return const AndroidAppInstallResult(launched: true);
|
|
66
|
+
}
|
|
67
|
+
return const AndroidAppInstallResult(
|
|
68
|
+
launched: false,
|
|
69
|
+
error: 'Android package installer could not be opened.',
|
|
70
|
+
);
|
|
71
|
+
} on PlatformException catch (error) {
|
|
72
|
+
return AndroidAppInstallResult(
|
|
73
|
+
launched: false,
|
|
74
|
+
error: error.message ?? error.code,
|
|
75
|
+
);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
return AndroidAppInstallResult(launched: false, error: error.toString());
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
Future<File> _downloadApk({
|
|
82
|
+
required String downloadUrl,
|
|
83
|
+
required String fileName,
|
|
84
|
+
required Map<String, String> headers,
|
|
85
|
+
}) async {
|
|
86
|
+
final request = http.Request('GET', Uri.parse(downloadUrl));
|
|
87
|
+
request.headers.addAll(headers);
|
|
88
|
+
final response = await _client.send(request);
|
|
89
|
+
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
90
|
+
throw HttpException(
|
|
91
|
+
'APK download failed with HTTP ${response.statusCode}.',
|
|
92
|
+
uri: Uri.parse(downloadUrl),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
final directory = await getTemporaryDirectory();
|
|
97
|
+
final updatesDir = Directory('${directory.path}/app_updates');
|
|
98
|
+
if (!updatesDir.existsSync()) {
|
|
99
|
+
updatesDir.createSync(recursive: true);
|
|
100
|
+
}
|
|
101
|
+
final sanitized = _sanitizeFileName(fileName);
|
|
102
|
+
final file = File('${updatesDir.path}/$sanitized');
|
|
103
|
+
if (file.existsSync()) {
|
|
104
|
+
file.deleteSync();
|
|
105
|
+
}
|
|
106
|
+
final sink = file.openWrite();
|
|
107
|
+
try {
|
|
108
|
+
await response.stream.pipe(sink);
|
|
109
|
+
} finally {
|
|
110
|
+
await sink.close();
|
|
111
|
+
}
|
|
112
|
+
return file;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
String _sanitizeFileName(String value) {
|
|
116
|
+
final trimmed = value.trim();
|
|
117
|
+
if (trimmed.isEmpty) {
|
|
118
|
+
return 'neoagent-update.apk';
|
|
119
|
+
}
|
|
120
|
+
return trimmed.replaceAll(RegExp(r'[^A-Za-z0-9._-]'), '_');
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import 'android_app_installer.dart';
|
|
2
|
+
|
|
3
|
+
AndroidAppInstaller createPlatformAndroidAppInstaller() =>
|
|
4
|
+
_UnsupportedAndroidAppInstaller();
|
|
5
|
+
|
|
6
|
+
class _UnsupportedAndroidAppInstaller implements AndroidAppInstaller {
|
|
7
|
+
@override
|
|
8
|
+
bool get supported => false;
|
|
9
|
+
|
|
10
|
+
@override
|
|
11
|
+
Future<AndroidAppInstallResult> installApkFromUrl({
|
|
12
|
+
required String downloadUrl,
|
|
13
|
+
required String fileName,
|
|
14
|
+
Map<String, String> headers = const <String, String>{},
|
|
15
|
+
}) async {
|
|
16
|
+
return const AndroidAppInstallResult(
|
|
17
|
+
launched: false,
|
|
18
|
+
error: 'Android APK install is unavailable on this platform.',
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
}
|