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.
Files changed (264) hide show
  1. package/.env.example +39 -0
  2. package/README.md +2 -0
  3. package/docs/capabilities.md +2 -2
  4. package/docs/configuration.md +13 -5
  5. package/docs/integrations.md +4 -1
  6. package/flutter_app/.metadata +42 -0
  7. package/flutter_app/README.md +21 -0
  8. package/flutter_app/analysis_options.yaml +32 -0
  9. package/flutter_app/android/app/build.gradle.kts +109 -0
  10. package/flutter_app/android/app/src/debug/AndroidManifest.xml +7 -0
  11. package/flutter_app/android/app/src/main/AndroidManifest.xml +147 -0
  12. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/MainActivity.kt +747 -0
  13. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthConnectGateway.kt +280 -0
  14. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncNotifications.kt +113 -0
  15. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncPayload.kt +57 -0
  16. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncScheduler.kt +78 -0
  17. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/HealthSyncWorker.kt +253 -0
  18. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/health/PermissionsRationaleActivity.kt +46 -0
  19. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingBootReceiver.kt +21 -0
  20. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingForegroundService.kt +586 -0
  21. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingStateStore.kt +78 -0
  22. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/recording/RecordingUploadClient.kt +104 -0
  23. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiHomeWidgetProvider.kt +457 -0
  24. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/AiWidgetStore.kt +194 -0
  25. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/VoiceLaunchWidgetProvider.kt +67 -0
  26. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetConfigActivity.kt +228 -0
  27. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncScheduler.kt +72 -0
  28. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetSyncWorker.kt +186 -0
  29. package/flutter_app/android/app/src/main/kotlin/com/neoagent/flutter_app/widgets/WidgetTaskRunWorker.kt +210 -0
  30. package/flutter_app/android/app/src/main/res/drawable/launch_background.xml +12 -0
  31. package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_bg.xml +11 -0
  32. package/flutter_app/android/app/src/main/res/drawable/neoagent_ai_widget_task_bg.xml +8 -0
  33. package/flutter_app/android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
  34. package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget.xml +138 -0
  35. package/flutter_app/android/app/src/main/res/layout/neoagent_ai_widget_task_row.xml +52 -0
  36. package/flutter_app/android/app/src/main/res/layout/neoagent_voice_widget.xml +49 -0
  37. package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  38. package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  39. package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  40. package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  41. package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  42. package/flutter_app/android/app/src/main/res/values/strings.xml +12 -0
  43. package/flutter_app/android/app/src/main/res/values/styles.xml +18 -0
  44. package/flutter_app/android/app/src/main/res/values-night/styles.xml +18 -0
  45. package/flutter_app/android/app/src/main/res/xml/file_paths.xml +6 -0
  46. package/flutter_app/android/app/src/main/res/xml/neoagent_ai_widget_info.xml +12 -0
  47. package/flutter_app/android/app/src/main/res/xml/neoagent_voice_widget_info.xml +12 -0
  48. package/flutter_app/android/app/src/profile/AndroidManifest.xml +7 -0
  49. package/flutter_app/android/build.gradle.kts +24 -0
  50. package/flutter_app/android/ci-release.keystore +0 -0
  51. package/flutter_app/android/gradle/wrapper/gradle-wrapper.properties +5 -0
  52. package/flutter_app/android/gradle.properties +3 -0
  53. package/flutter_app/android/key.properties +4 -0
  54. package/flutter_app/android/settings.gradle.kts +26 -0
  55. package/flutter_app/assets/branding/app_icon_1024.png +0 -0
  56. package/flutter_app/assets/branding/app_icon_128.png +0 -0
  57. package/flutter_app/assets/branding/app_icon_192.png +0 -0
  58. package/flutter_app/assets/branding/app_icon_256.png +0 -0
  59. package/flutter_app/assets/branding/app_icon_32.png +0 -0
  60. package/flutter_app/assets/branding/app_icon_512.png +0 -0
  61. package/flutter_app/assets/branding/app_icon_64.png +0 -0
  62. package/flutter_app/assets/branding/tray_icon_template.png +0 -0
  63. package/flutter_app/lib/features/location/location_service.dart +119 -0
  64. package/flutter_app/lib/features/notifications/notification_interceptor.dart +97 -0
  65. package/flutter_app/lib/main.dart +23057 -0
  66. package/flutter_app/lib/main_app_shell.dart +1682 -0
  67. package/flutter_app/lib/main_integrations.dart +931 -0
  68. package/flutter_app/lib/main_launcher.dart +959 -0
  69. package/flutter_app/lib/main_launcher_entry.dart +5 -0
  70. package/flutter_app/lib/main_models.dart +3473 -0
  71. package/flutter_app/lib/main_shared.dart +2861 -0
  72. package/flutter_app/lib/main_theme.dart +204 -0
  73. package/flutter_app/lib/main_voice_assistant.dart +831 -0
  74. package/flutter_app/lib/src/android_apk_drop_zone.dart +32 -0
  75. package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +16 -0
  76. package/flutter_app/lib/src/android_apk_drop_zone_web.dart +348 -0
  77. package/flutter_app/lib/src/android_app_installer.dart +22 -0
  78. package/flutter_app/lib/src/android_app_installer_io.dart +122 -0
  79. package/flutter_app/lib/src/android_app_installer_stub.dart +21 -0
  80. package/flutter_app/lib/src/android_launcher_bridge.dart +239 -0
  81. package/flutter_app/lib/src/app_launch_bridge.dart +29 -0
  82. package/flutter_app/lib/src/app_release_updater.dart +511 -0
  83. package/flutter_app/lib/src/backend_client.dart +1833 -0
  84. package/flutter_app/lib/src/desktop_companion.dart +2 -0
  85. package/flutter_app/lib/src/desktop_companion_actions.dart +586 -0
  86. package/flutter_app/lib/src/desktop_companion_io.dart +538 -0
  87. package/flutter_app/lib/src/desktop_companion_stub.dart +59 -0
  88. package/flutter_app/lib/src/desktop_native_bridge.dart +91 -0
  89. package/flutter_app/lib/src/desktop_screen_capture.dart +21 -0
  90. package/flutter_app/lib/src/desktop_screen_capture_io.dart +142 -0
  91. package/flutter_app/lib/src/desktop_screen_capture_stub.dart +12 -0
  92. package/flutter_app/lib/src/diagnostics_logger.dart +119 -0
  93. package/flutter_app/lib/src/health_bridge.dart +136 -0
  94. package/flutter_app/lib/src/live_voice_capture.dart +85 -0
  95. package/flutter_app/lib/src/messaging_access_summary.dart +46 -0
  96. package/flutter_app/lib/src/network/app_http_client.dart +53 -0
  97. package/flutter_app/lib/src/network/app_http_client_factory.dart +6 -0
  98. package/flutter_app/lib/src/network/app_http_client_io.dart +138 -0
  99. package/flutter_app/lib/src/network/app_http_client_stub.dart +3 -0
  100. package/flutter_app/lib/src/network/app_http_client_web.dart +94 -0
  101. package/flutter_app/lib/src/oauth_launcher.dart +33 -0
  102. package/flutter_app/lib/src/oauth_launcher_io.dart +77 -0
  103. package/flutter_app/lib/src/oauth_launcher_stub.dart +33 -0
  104. package/flutter_app/lib/src/oauth_launcher_web.dart +107 -0
  105. package/flutter_app/lib/src/recording_bridge.dart +232 -0
  106. package/flutter_app/lib/src/recording_bridge_io.dart +1019 -0
  107. package/flutter_app/lib/src/recording_bridge_stub.dart +120 -0
  108. package/flutter_app/lib/src/recording_bridge_web.dart +689 -0
  109. package/flutter_app/lib/src/recording_payloads.dart +86 -0
  110. package/flutter_app/lib/src/theme/palette.dart +81 -0
  111. package/flutter_app/lib/src/widget_bridge.dart +49 -0
  112. package/flutter_app/linux/CMakeLists.txt +128 -0
  113. package/flutter_app/linux/flutter/CMakeLists.txt +88 -0
  114. package/flutter_app/linux/flutter/generated_plugin_registrant.cc +43 -0
  115. package/flutter_app/linux/flutter/generated_plugin_registrant.h +15 -0
  116. package/flutter_app/linux/flutter/generated_plugins.cmake +31 -0
  117. package/flutter_app/linux/runner/CMakeLists.txt +26 -0
  118. package/flutter_app/linux/runner/main.cc +6 -0
  119. package/flutter_app/linux/runner/my_application.cc +144 -0
  120. package/flutter_app/linux/runner/my_application.h +18 -0
  121. package/flutter_app/linux/runner/resources/app_icon.png +0 -0
  122. package/flutter_app/macos/Flutter/Flutter-Debug.xcconfig +2 -0
  123. package/flutter_app/macos/Flutter/Flutter-Release.xcconfig +2 -0
  124. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +40 -0
  125. package/flutter_app/macos/Podfile +42 -0
  126. package/flutter_app/macos/Podfile.lock +87 -0
  127. package/flutter_app/macos/Runner/AppDelegate.swift +576 -0
  128. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -0
  129. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  130. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  131. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  132. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  133. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  134. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  135. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  136. package/flutter_app/macos/Runner/Base.lproj/MainMenu.xib +342 -0
  137. package/flutter_app/macos/Runner/Configs/AppInfo.xcconfig +14 -0
  138. package/flutter_app/macos/Runner/Configs/Debug.xcconfig +2 -0
  139. package/flutter_app/macos/Runner/Configs/Release.xcconfig +2 -0
  140. package/flutter_app/macos/Runner/Configs/Warnings.xcconfig +13 -0
  141. package/flutter_app/macos/Runner/DebugProfile.entitlements +16 -0
  142. package/flutter_app/macos/Runner/Info.plist +36 -0
  143. package/flutter_app/macos/Runner/MainFlutterWindow.swift +19 -0
  144. package/flutter_app/macos/Runner/Release.entitlements +12 -0
  145. package/flutter_app/macos/Runner.xcodeproj/project.pbxproj +801 -0
  146. package/flutter_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  147. package/flutter_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +99 -0
  148. package/flutter_app/macos/Runner.xcworkspace/contents.xcworkspacedata +10 -0
  149. package/flutter_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  150. package/flutter_app/macos/RunnerTests/RunnerTests.swift +12 -0
  151. package/flutter_app/patch_strings.py +12 -0
  152. package/flutter_app/pubspec.lock +1088 -0
  153. package/flutter_app/pubspec.yaml +53 -0
  154. package/flutter_app/test/messaging_access_summary_test.dart +22 -0
  155. package/flutter_app/test/recording_payloads_test.dart +53 -0
  156. package/flutter_app/third_party/desktop_audio_capture/LICENSE +21 -0
  157. package/flutter_app/third_party/desktop_audio_capture/README.md +262 -0
  158. package/flutter_app/third_party/desktop_audio_capture/lib/audio_capture.dart +65 -0
  159. package/flutter_app/third_party/desktop_audio_capture/lib/config/mic_audio_config.dart +153 -0
  160. package/flutter_app/third_party/desktop_audio_capture/lib/config/system_adudio_config.dart +110 -0
  161. package/flutter_app/third_party/desktop_audio_capture/lib/mic/mic_audio_capture.dart +461 -0
  162. package/flutter_app/third_party/desktop_audio_capture/lib/model/audio_status.dart +91 -0
  163. package/flutter_app/third_party/desktop_audio_capture/lib/model/decibel_data.dart +106 -0
  164. package/flutter_app/third_party/desktop_audio_capture/lib/model/input_device_type.dart +219 -0
  165. package/flutter_app/third_party/desktop_audio_capture/lib/system/system_audio_capture.dart +336 -0
  166. package/flutter_app/third_party/desktop_audio_capture/linux/CMakeLists.txt +101 -0
  167. package/flutter_app/third_party/desktop_audio_capture/linux/audio_capture_plugin.cc +692 -0
  168. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/audio_capture_plugin.h +35 -0
  169. package/flutter_app/third_party/desktop_audio_capture/linux/include/audio_capture/mic_capture_plugin.h +36 -0
  170. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/audio_capture_plugin.h +32 -0
  171. package/flutter_app/third_party/desktop_audio_capture/linux/include/desktop_audio_capture/mic_capture_plugin.h +32 -0
  172. package/flutter_app/third_party/desktop_audio_capture/linux/mic_capture_plugin.cc +878 -0
  173. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/AudioCapturePlugin.swift +27 -0
  174. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/MicCapturePlugin.swift +1172 -0
  175. package/flutter_app/third_party/desktop_audio_capture/macos/Classes/SystemCapturePlugin.swift +655 -0
  176. package/flutter_app/third_party/desktop_audio_capture/macos/Resources/PrivacyInfo.xcprivacy +12 -0
  177. package/flutter_app/third_party/desktop_audio_capture/macos/desktop_audio_capture.podspec +30 -0
  178. package/flutter_app/third_party/desktop_audio_capture/pubspec.yaml +87 -0
  179. package/flutter_app/third_party/desktop_audio_capture/windows/CMakeLists.txt +105 -0
  180. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.cpp +80 -0
  181. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin.h +31 -0
  182. package/flutter_app/third_party/desktop_audio_capture/windows/audio_capture_plugin_c_api.cpp +12 -0
  183. package/flutter_app/third_party/desktop_audio_capture/windows/include/audio_capture/audio_capture_plugin_c_api.h +23 -0
  184. package/flutter_app/third_party/desktop_audio_capture/windows/include/desktop_audio_capture/audio_capture_plugin.h +25 -0
  185. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.cpp +1117 -0
  186. package/flutter_app/third_party/desktop_audio_capture/windows/mic_capture_plugin.h +115 -0
  187. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.cpp +777 -0
  188. package/flutter_app/third_party/desktop_audio_capture/windows/system_audio_capture_plugin.h +87 -0
  189. package/flutter_app/third_party/flutter_secure_storage_linux/linux/CMakeLists.txt +30 -0
  190. package/flutter_app/third_party/flutter_secure_storage_linux/linux/flutter_secure_storage_linux_plugin.cc +215 -0
  191. package/flutter_app/third_party/flutter_secure_storage_linux/linux/include/flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h +27 -0
  192. package/flutter_app/third_party/flutter_secure_storage_linux/pubspec.yaml +20 -0
  193. package/flutter_app/tool/generate_desktop_branding.py +219 -0
  194. package/flutter_app/web/favicon.png +0 -0
  195. package/flutter_app/web/favicon.svg +12 -0
  196. package/flutter_app/web/icons/Icon-192.png +0 -0
  197. package/flutter_app/web/icons/Icon-512.png +0 -0
  198. package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
  199. package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
  200. package/flutter_app/web/index.html +39 -0
  201. package/flutter_app/web/manifest.json +35 -0
  202. package/flutter_app/windows/CMakeLists.txt +108 -0
  203. package/flutter_app/windows/flutter/CMakeLists.txt +109 -0
  204. package/flutter_app/windows/flutter/generated_plugin_registrant.cc +47 -0
  205. package/flutter_app/windows/flutter/generated_plugin_registrant.h +15 -0
  206. package/flutter_app/windows/flutter/generated_plugins.cmake +35 -0
  207. package/flutter_app/windows/runner/CMakeLists.txt +41 -0
  208. package/flutter_app/windows/runner/Runner.rc +121 -0
  209. package/flutter_app/windows/runner/flutter_window.cpp +533 -0
  210. package/flutter_app/windows/runner/flutter_window.h +37 -0
  211. package/flutter_app/windows/runner/main.cpp +53 -0
  212. package/flutter_app/windows/runner/resource.h +16 -0
  213. package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
  214. package/flutter_app/windows/runner/runner.exe.manifest +14 -0
  215. package/flutter_app/windows/runner/utils.cpp +65 -0
  216. package/flutter_app/windows/runner/utils.h +19 -0
  217. package/flutter_app/windows/runner/win32_window.cpp +299 -0
  218. package/flutter_app/windows/runner/win32_window.h +102 -0
  219. package/lib/manager.js +231 -7
  220. package/package.json +3 -1
  221. package/server/db/database.js +68 -0
  222. package/server/http/middleware.js +50 -0
  223. package/server/http/routes.js +3 -1
  224. package/server/index.js +1 -0
  225. package/server/public/.last_build_id +1 -1
  226. package/server/public/assets/NOTICES +61 -0
  227. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  228. package/server/public/flutter_bootstrap.js +1 -1
  229. package/server/public/main.dart.js +65262 -64422
  230. package/server/routes/integrations.js +86 -0
  231. package/server/routes/memory.js +11 -2
  232. package/server/routes/screenHistory.js +46 -0
  233. package/server/routes/triggers.js +81 -0
  234. package/server/services/ai/models.js +30 -0
  235. package/server/services/ai/providers/githubCopilot.js +97 -0
  236. package/server/services/ai/providers/openai.js +2 -1
  237. package/server/services/ai/providers/openaiCodex.js +31 -0
  238. package/server/services/ai/settings.js +20 -0
  239. package/server/services/ai/systemPrompt.js +1 -1
  240. package/server/services/ai/tools.js +35 -6
  241. package/server/services/browser/controller.js +47 -3
  242. package/server/services/desktop/screenRecorder.js +172 -0
  243. package/server/services/integrations/env.js +5 -0
  244. package/server/services/integrations/github/common.js +106 -0
  245. package/server/services/integrations/github/provider.js +499 -0
  246. package/server/services/integrations/github/repos.js +1124 -0
  247. package/server/services/integrations/home_assistant/provider.js +306 -26
  248. package/server/services/integrations/manager.js +63 -7
  249. package/server/services/integrations/oauth_provider.js +13 -6
  250. package/server/services/integrations/provider_config_store.js +76 -0
  251. package/server/services/integrations/registry.js +4 -0
  252. package/server/services/integrations/trello/provider.js +744 -0
  253. package/server/services/integrations/whatsapp/provider.js +6 -2
  254. package/server/services/manager.js +22 -0
  255. package/server/services/memory/manager.js +39 -2
  256. package/server/services/skills/base_catalog.js +33 -0
  257. package/server/services/tasks/adapters/index.js +1 -0
  258. package/server/services/tasks/adapters/manual.js +12 -0
  259. package/server/services/tasks/runtime.js +1 -1
  260. package/server/services/voice/openaiClient.js +4 -1
  261. package/server/services/voice/providers.js +2 -1
  262. package/server/services/widgets/service.js +49 -4
  263. package/server/utils/local_secrets.js +56 -0
  264. package/server/utils/logger.js +37 -9
@@ -0,0 +1,655 @@
1
+ import Cocoa
2
+ import FlutterMacOS
3
+ import AVFoundation
4
+ @preconcurrency import ScreenCaptureKit
5
+
6
+ // MARK: - Error Types
7
+ enum CaptureError: Error {
8
+ case noPermission
9
+ case noDisplay
10
+ case alreadyCapturing
11
+ case notCapturing
12
+ case invalidConfiguration(String)
13
+ case streamCreationFailed
14
+ case captureStartFailed(Error)
15
+ case captureStopFailed(Error)
16
+
17
+ var message: String {
18
+ switch self {
19
+ case .noPermission:
20
+ return "Screen recording permission not granted"
21
+ case .noDisplay:
22
+ return "No display found"
23
+ case .alreadyCapturing:
24
+ return "Capture already in progress"
25
+ case .notCapturing:
26
+ return "No active capture session"
27
+ case .invalidConfiguration(let detail):
28
+ return "Invalid configuration: \(detail)"
29
+ case .streamCreationFailed:
30
+ return "Failed to create capture stream"
31
+ case .captureStartFailed(let error):
32
+ return "Failed to start capture: \(error.localizedDescription)"
33
+ case .captureStopFailed(let error):
34
+ return "Failed to stop capture: \(error.localizedDescription)"
35
+ }
36
+ }
37
+ }
38
+
39
+ // MARK: - Audio Configuration
40
+ struct AudioConfiguration {
41
+ let sampleRate: Double
42
+ let channelCount: Int
43
+
44
+ static let `default` = AudioConfiguration(sampleRate: 16000, channelCount: 1)
45
+
46
+ static func from(_ dict: [String: Any]?) -> Result<AudioConfiguration, CaptureError> {
47
+ guard let dict = dict else {
48
+ return .success(.default)
49
+ }
50
+
51
+ let sampleRate = (dict["sampleRate"] as? NSNumber)?.doubleValue ?? 16000
52
+ let channelCount = (dict["channels"] as? NSNumber)?.intValue ?? 1
53
+
54
+ // Validate
55
+ guard [8000, 16000, 44100, 48000].contains(Int(sampleRate)) else {
56
+ return .failure(.invalidConfiguration("Sample rate must be 8000, 16000, 44100, or 48000"))
57
+ }
58
+
59
+ guard (1...2).contains(channelCount) else {
60
+ return .failure(.invalidConfiguration("Channel count must be 1 or 2"))
61
+ }
62
+
63
+ return .success(AudioConfiguration(sampleRate: sampleRate, channelCount: channelCount))
64
+ }
65
+ }
66
+
67
+ // MARK: - Main Plugin
68
+ @available(macOS 13.0, *)
69
+ final class SystemCapturePlugin: NSObject, FlutterPlugin, @unchecked Sendable {
70
+ private var methodChannel: FlutterMethodChannel?
71
+ private var eventChannel: FlutterEventChannel?
72
+ private var eventSink: FlutterEventSink?
73
+
74
+ private var statusEventChannel: FlutterEventChannel?
75
+ var statusEventSink: FlutterEventSink? // Changed to var for access from handler
76
+
77
+ private var decibelEventChannel: FlutterEventChannel?
78
+ var decibelEventSink: FlutterEventSink?
79
+
80
+ private var stream: SCStream?
81
+ var streamOutput: StreamOutput? // Internal access for stream handlers
82
+
83
+ // Thread-safe state management using serial queue
84
+ private let stateQueue = DispatchQueue(label: "com.system_audio_transcriber.state_queue", qos: .utility)
85
+ private var _isCapturing = false
86
+ var isCapturing: Bool {
87
+ get {
88
+ return stateQueue.sync { _isCapturing }
89
+ }
90
+ set {
91
+ stateQueue.async { [weak self] in
92
+ self?._isCapturing = newValue
93
+ }
94
+ }
95
+ }
96
+
97
+ private let captureQueue = DispatchQueue(label: "com.system_audio_transcriber.capture_queue", qos: .userInitiated)
98
+
99
+ static func register(with registrar: FlutterPluginRegistrar) {
100
+ let instance = SystemCapturePlugin()
101
+
102
+ let methodChannel = FlutterMethodChannel(
103
+ name: "com.system_audio_transcriber/audio_capture",
104
+ binaryMessenger: registrar.messenger
105
+ )
106
+ instance.methodChannel = methodChannel
107
+ registrar.addMethodCallDelegate(instance, channel: methodChannel)
108
+
109
+ let eventChannel = FlutterEventChannel(
110
+ name: "com.system_audio_transcriber/audio_stream",
111
+ binaryMessenger: registrar.messenger
112
+ )
113
+ instance.eventChannel = eventChannel
114
+ eventChannel.setStreamHandler(instance)
115
+
116
+ let statusEventChannel = FlutterEventChannel(
117
+ name: "com.system_audio_transcriber/audio_status",
118
+ binaryMessenger: registrar.messenger
119
+ )
120
+ instance.statusEventChannel = statusEventChannel
121
+ statusEventChannel.setStreamHandler(SystemStatusStreamHandler(plugin: instance))
122
+
123
+ let decibelEventChannel = FlutterEventChannel(
124
+ name: "com.system_audio_transcriber/audio_decibel",
125
+ binaryMessenger: registrar.messenger
126
+ )
127
+ instance.decibelEventChannel = decibelEventChannel
128
+ decibelEventChannel.setStreamHandler(SystemDecibelStreamHandler(plugin: instance))
129
+
130
+ // Register for app termination to cleanup
131
+ NotificationCenter.default.addObserver(
132
+ instance,
133
+ selector: #selector(instance.applicationWillTerminate),
134
+ name: NSApplication.willTerminateNotification,
135
+ object: nil
136
+ )
137
+ }
138
+
139
+ @objc private func applicationWillTerminate() {
140
+ Task {
141
+ await cleanupResources()
142
+ }
143
+ }
144
+
145
+ deinit {
146
+ NotificationCenter.default.removeObserver(self)
147
+ }
148
+
149
+ func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
150
+ switch call.method {
151
+
152
+ case "requestPermissions":
153
+ requestPermissions(result: result)
154
+
155
+ case "startCapture":
156
+ let config = call.arguments as? [String: Any]
157
+ Task {
158
+ await startCapture(config: config, result: result)
159
+ }
160
+
161
+ case "stopCapture":
162
+ Task {
163
+ await stopCapture(result: result)
164
+ }
165
+
166
+ default:
167
+ result(FlutterMethodNotImplemented)
168
+ }
169
+ }
170
+
171
+ private func requestPermissions(result: @escaping FlutterResult) {
172
+ let hasPermission = CGPreflightScreenCaptureAccess()
173
+
174
+ if hasPermission {
175
+ result(true)
176
+ return
177
+ }
178
+
179
+ let granted = CGRequestScreenCaptureAccess()
180
+
181
+ if granted {
182
+ result(true)
183
+ } else {
184
+ DispatchQueue.main.async { [weak self] in
185
+ self?.showPermissionAlert()
186
+ }
187
+ result(false)
188
+ }
189
+ }
190
+
191
+ private func showPermissionAlert() {
192
+ let alert = NSAlert()
193
+ alert.messageText = "Screen Recording Permission Required"
194
+ alert.informativeText = """
195
+ This app needs Screen Recording permission to capture system audio.
196
+
197
+ Please follow these steps:
198
+ 1. Click "Open System Settings" below
199
+ 2. In Privacy & Security → Screen Recording
200
+ 3. Enable the toggle for this app
201
+ 4. Restart the app
202
+ """
203
+ alert.alertStyle = .warning
204
+ alert.addButton(withTitle: "Open System Settings")
205
+ alert.addButton(withTitle: "Cancel")
206
+
207
+ let response = alert.runModal()
208
+ if response == .alertFirstButtonReturn {
209
+ if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture") {
210
+ NSWorkspace.shared.open(url)
211
+ }
212
+ }
213
+ }
214
+
215
+ private func startCapture(config: [String: Any]?, result: @escaping FlutterResult) async {
216
+ // Check if already capturing
217
+ if isCapturing {
218
+ print("⚠️ Already capturing, stopping first...")
219
+ await cleanupResources()
220
+ // Wait for cleanup
221
+ try? await Task.sleep(nanoseconds: 100_000_000)
222
+ }
223
+
224
+ // Check permission
225
+ guard CGPreflightScreenCaptureAccess() else {
226
+ print("❌ No screen recording permission")
227
+ let errorMessage = CaptureError.noPermission.message
228
+ DispatchQueue.main.async { [weak self] in
229
+ self?.showPermissionAlert()
230
+ result(FlutterError(
231
+ code: "NO_PERMISSION",
232
+ message: errorMessage,
233
+ details: nil
234
+ ))
235
+ }
236
+ return
237
+ }
238
+
239
+ // Parse and validate configuration
240
+ let audioConfig: AudioConfiguration
241
+ switch AudioConfiguration.from(config) {
242
+ case .success(let cfg):
243
+ audioConfig = cfg
244
+ case .failure(let error):
245
+ DispatchQueue.main.async {
246
+ result(FlutterError(
247
+ code: "INVALID_CONFIG",
248
+ message: error.message,
249
+ details: nil
250
+ ))
251
+ }
252
+ return
253
+ }
254
+
255
+ do {
256
+ print("🎬 Starting capture with config: \(audioConfig.sampleRate)Hz, \(audioConfig.channelCount)ch")
257
+
258
+ // Get shareable content
259
+ let availableContent = try await SCShareableContent.excludingDesktopWindows(
260
+ false,
261
+ onScreenWindowsOnly: true
262
+ )
263
+
264
+ guard let display = availableContent.displays.first else {
265
+ throw CaptureError.noDisplay
266
+ }
267
+
268
+ print("📺 Display: \(display.displayID)")
269
+
270
+ // Configure stream
271
+ let configuration = SCStreamConfiguration()
272
+ configuration.capturesAudio = true
273
+ configuration.sampleRate = Int(audioConfig.sampleRate)
274
+ configuration.channelCount = audioConfig.channelCount
275
+ configuration.excludesCurrentProcessAudio = true
276
+
277
+ // Video settings - minimal to reduce overhead
278
+ // ScreenCaptureKit requires video output even for audio-only capture
279
+ configuration.width = 100
280
+ configuration.height = 100
281
+ configuration.minimumFrameInterval = CMTime(value: 1, timescale: 1) // 1 FPS
282
+ configuration.queueDepth = 3
283
+ configuration.pixelFormat = kCVPixelFormatType_32BGRA
284
+ configuration.showsCursor = false
285
+
286
+ // Create filter and stream
287
+ let filter = SCContentFilter(display: display, excludingWindows: [])
288
+ let newStreamOutput = StreamOutput(eventSink: eventSink, decibelEventSink: decibelEventSink)
289
+ let newStream = SCStream(filter: filter, configuration: configuration, delegate: nil)
290
+ let stream = newStream
291
+
292
+ // Add output handlers
293
+ // Note: macOS 13.x requires both audio and video handlers even if video is minimal
294
+ try stream.addStreamOutput(newStreamOutput, type: .audio, sampleHandlerQueue: .main)
295
+
296
+ // Only add video handler if not explicitly disabled (macOS 13.x compatibility)
297
+ if #available(macOS 14.0, *) {
298
+ // No video handler needed on macOS 14+ when capturesVideo = false
299
+ } else {
300
+ try stream.addStreamOutput(newStreamOutput, type: .screen, sampleHandlerQueue: .main)
301
+ }
302
+
303
+ // Start capture
304
+ try await stream.startCapture()
305
+
306
+ // Update state atomically on state queue
307
+ // Use sync to avoid Sendable capture issues - we're already on async context
308
+ stateQueue.sync { [weak self] in
309
+ guard let self = self else { return }
310
+ self.stream = stream
311
+ self.streamOutput = newStreamOutput
312
+ self._isCapturing = true
313
+ }
314
+
315
+ // Notify status change
316
+ sendStatusUpdate(isActive: true)
317
+
318
+ print("✅ Capture started successfully")
319
+ DispatchQueue.main.async {
320
+ result(true)
321
+ }
322
+
323
+ } catch let error as CaptureError {
324
+ print("❌ Capture error: \(error.message)")
325
+ await cleanupResources()
326
+ DispatchQueue.main.async {
327
+ result(FlutterError(
328
+ code: "CAPTURE_ERROR",
329
+ message: error.message,
330
+ details: nil
331
+ ))
332
+ }
333
+ } catch {
334
+ print("❌ Unexpected error: \(error)")
335
+ await cleanupResources()
336
+ DispatchQueue.main.async {
337
+ result(FlutterError(
338
+ code: "CAPTURE_ERROR",
339
+ message: CaptureError.captureStartFailed(error).message,
340
+ details: "\(error)"
341
+ ))
342
+ }
343
+ }
344
+ }
345
+
346
+ private func stopCapture(result: @escaping FlutterResult) async {
347
+ guard isCapturing else {
348
+ DispatchQueue.main.async {
349
+ result(FlutterError(
350
+ code: "NOT_CAPTURING",
351
+ message: CaptureError.notCapturing.message,
352
+ details: nil
353
+ ))
354
+ }
355
+ return
356
+ }
357
+
358
+ await cleanupResources()
359
+
360
+ print("✅ Capture stopped")
361
+ DispatchQueue.main.async {
362
+ result(true)
363
+ }
364
+ }
365
+
366
+ // Centralized cleanup - idempotent and thread-safe
367
+ private func cleanupResources() async {
368
+ // Get current stream and output atomically
369
+ let (currentStream, currentOutput) = await withCheckedContinuation { (continuation: CheckedContinuation<(SCStream?, StreamOutput?), Never>) in
370
+ stateQueue.async { [weak self] in
371
+ guard let self = self else {
372
+ continuation.resume(returning: (nil, nil))
373
+ return
374
+ }
375
+ continuation.resume(returning: (self.stream, self.streamOutput))
376
+ }
377
+ }
378
+
379
+ guard let stream = currentStream else {
380
+ // Already cleaned up
381
+ await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
382
+ stateQueue.async { [weak self] in
383
+ self?._isCapturing = false
384
+ continuation.resume()
385
+ }
386
+ }
387
+ return
388
+ }
389
+
390
+ do {
391
+ // Remove outputs
392
+ if let output = currentOutput {
393
+ try stream.removeStreamOutput(output, type: .audio)
394
+
395
+ // Only remove video handler if it was added (macOS 13.x)
396
+ if #available(macOS 14.0, *) {
397
+ // No video handler to remove
398
+ } else {
399
+ try stream.removeStreamOutput(output, type: .screen)
400
+ }
401
+ }
402
+
403
+ // Stop stream
404
+ try await stream.stopCapture()
405
+
406
+ // Small delay for graceful shutdown
407
+ try? await Task.sleep(nanoseconds: 50_000_000)
408
+
409
+ } catch {
410
+ print("⚠️ Cleanup error: \(error.localizedDescription)")
411
+ }
412
+
413
+ // Clear state atomically
414
+ await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
415
+ stateQueue.async { [weak self] in
416
+ guard let self = self else {
417
+ continuation.resume()
418
+ return
419
+ }
420
+ self.stream = nil
421
+ self.streamOutput = nil
422
+ self._isCapturing = false
423
+ continuation.resume()
424
+ }
425
+ }
426
+
427
+ // Notify status change
428
+ sendStatusUpdate(isActive: false)
429
+ }
430
+
431
+ private func sendStatusUpdate(isActive: Bool) {
432
+ DispatchQueue.main.async { [weak self] in
433
+ self?.statusEventSink?([
434
+ "isActive": isActive,
435
+ "timestamp": Date().timeIntervalSince1970
436
+ ])
437
+ }
438
+ }
439
+ }
440
+
441
+ // MARK: - FlutterStreamHandler
442
+ @available(macOS 13.0, *)
443
+ extension SystemCapturePlugin: FlutterStreamHandler {
444
+ func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
445
+ self.eventSink = events
446
+ streamOutput?.eventSink = events
447
+ return nil
448
+ }
449
+
450
+ func onCancel(withArguments arguments: Any?) -> FlutterError? {
451
+ self.eventSink = nil
452
+ streamOutput?.eventSink = nil
453
+ return nil
454
+ }
455
+ }
456
+
457
+ // MARK: - Stream Output Handler
458
+ @available(macOS 13.0, *)
459
+ final class StreamOutput: NSObject, SCStreamOutput, @unchecked Sendable {
460
+ var eventSink: FlutterEventSink?
461
+ var decibelEventSink: FlutterEventSink?
462
+ private static var hasLoggedFormat = false
463
+ private let processingQueue = DispatchQueue(label: "com.system_audio_transcriber.processing", qos: .userInitiated)
464
+
465
+ init(eventSink: FlutterEventSink?, decibelEventSink: FlutterEventSink? = nil) {
466
+ self.eventSink = eventSink
467
+ self.decibelEventSink = decibelEventSink
468
+ super.init()
469
+ }
470
+
471
+ func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer, of type: SCStreamOutputType) {
472
+ // Only process audio, ignore video frames
473
+ guard type == .audio else { return }
474
+
475
+ // Process on background queue
476
+ processingQueue.async { [weak self] in
477
+ guard let self = self else { return }
478
+
479
+ guard let audioData = self.extractAudioData(from: sampleBuffer) else {
480
+ return
481
+ }
482
+
483
+ // Calculate decibel from audio data
484
+ let decibel = self.calculateDecibel(from: audioData)
485
+
486
+ // Send to Flutter on main thread
487
+ DispatchQueue.main.async { [weak self] in
488
+ if let sink = self?.eventSink {
489
+ sink(FlutterStandardTypedData(bytes: audioData))
490
+ }
491
+
492
+ // Send decibel data
493
+ if let decibelSink = self?.decibelEventSink {
494
+ decibelSink([
495
+ "decibel": decibel,
496
+ "timestamp": Date().timeIntervalSince1970
497
+ ])
498
+ }
499
+ }
500
+ }
501
+ }
502
+
503
+ private func extractAudioData(from sampleBuffer: CMSampleBuffer) -> Data? {
504
+ guard let blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer),
505
+ let formatDescription = CMSampleBufferGetFormatDescription(sampleBuffer),
506
+ let audioStreamBasicDescription = CMAudioFormatDescriptionGetStreamBasicDescription(formatDescription) else {
507
+ return nil
508
+ }
509
+
510
+ // Log format once
511
+ if !StreamOutput.hasLoggedFormat {
512
+ StreamOutput.hasLoggedFormat = true
513
+ let desc = audioStreamBasicDescription.pointee
514
+ print("🎤 Audio Format:")
515
+ print(" Sample Rate: \(desc.mSampleRate) Hz")
516
+ print(" Channels: \(desc.mChannelsPerFrame)")
517
+ print(" Bits/Channel: \(desc.mBitsPerChannel)")
518
+ print(" Format ID: \(desc.mFormatID)")
519
+ print(" Format Flags: \(desc.mFormatFlags)")
520
+ }
521
+
522
+ var length: Int = 0
523
+ var dataPointer: UnsafeMutablePointer<Int8>?
524
+
525
+ let status = CMBlockBufferGetDataPointer(
526
+ blockBuffer,
527
+ atOffset: 0,
528
+ lengthAtOffsetOut: nil,
529
+ totalLengthOut: &length,
530
+ dataPointerOut: &dataPointer
531
+ )
532
+
533
+ guard status == kCMBlockBufferNoErr, let pointer = dataPointer else {
534
+ return nil
535
+ }
536
+
537
+ let desc = audioStreamBasicDescription.pointee
538
+
539
+ // Float32 to Int16 conversion
540
+ if desc.mFormatID == kAudioFormatLinearPCM && desc.mFormatFlags & kAudioFormatFlagIsFloat != 0 {
541
+ return convertFloat32ToInt16(pointer: pointer, length: length)
542
+ }
543
+
544
+ // Already Int16
545
+ if desc.mFormatID == kAudioFormatLinearPCM && desc.mBitsPerChannel == 16 {
546
+ return Data(bytes: pointer, count: length)
547
+ }
548
+
549
+ print("⚠️ Unsupported audio format: \(desc.mFormatID)")
550
+ return nil
551
+ }
552
+
553
+ private func convertFloat32ToInt16(pointer: UnsafeMutablePointer<Int8>, length: Int) -> Data {
554
+ let floatPointer = pointer.withMemoryRebound(to: Float32.self, capacity: length / MemoryLayout<Float32>.size) { $0 }
555
+ let sampleCount = length / MemoryLayout<Float32>.size
556
+
557
+ var int16Data = Data(capacity: sampleCount * MemoryLayout<Int16>.size)
558
+
559
+ for i in 0..<sampleCount {
560
+ // Clamp and convert
561
+ let sample = min(max(floatPointer[i], -1.0), 1.0)
562
+ let int16Sample = Int16(sample * 32767.0)
563
+ withUnsafeBytes(of: int16Sample) { int16Data.append(contentsOf: $0) }
564
+ }
565
+
566
+ return int16Data
567
+ }
568
+
569
+ /// Calculate decibel (dB) from Int16 PCM audio data
570
+ /// Returns RMS-based decibel value, typically ranges from -∞ to 0 dB
571
+ private func calculateDecibel(from audioData: Data) -> Double {
572
+ guard audioData.count >= 2 else { return -120.0 } // Silence threshold
573
+
574
+ // Convert Data to Int16 array
575
+ let sampleCount = audioData.count / MemoryLayout<Int16>.size
576
+ var samples: [Int16] = []
577
+ samples.reserveCapacity(sampleCount)
578
+
579
+ audioData.withUnsafeBytes { bytes in
580
+ let int16Pointer = bytes.bindMemory(to: Int16.self)
581
+ for i in 0..<sampleCount {
582
+ samples.append(int16Pointer[i])
583
+ }
584
+ }
585
+
586
+ guard !samples.isEmpty else { return -120.0 }
587
+
588
+ // Calculate RMS (Root Mean Square)
589
+ let sumOfSquares = samples.reduce(0.0) { sum, sample in
590
+ let value = Double(sample)
591
+ return sum + (value * value)
592
+ }
593
+ let meanSquare = sumOfSquares / Double(samples.count)
594
+ let rms = sqrt(meanSquare)
595
+
596
+ // Calculate decibel: dB = 20 * log10(RMS / max_value)
597
+ // For Int16, max_value is 32767.0
598
+ let maxValue = 32767.0
599
+ guard rms > 0 else { return -120.0 } // Avoid log(0)
600
+
601
+ let decibel = 20.0 * log10(rms / maxValue)
602
+
603
+ // Clamp to reasonable range (-120 dB to 0 dB)
604
+ return max(-120.0, min(0.0, decibel))
605
+ }
606
+ }
607
+
608
+ // MARK: - System Status Stream Handler
609
+ @available(macOS 13.0, *)
610
+ class SystemStatusStreamHandler: NSObject, FlutterStreamHandler {
611
+ weak var plugin: SystemCapturePlugin?
612
+
613
+ init(plugin: SystemCapturePlugin) {
614
+ self.plugin = plugin
615
+ }
616
+
617
+ func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
618
+ plugin?.statusEventSink = events
619
+ // Send current status immediately
620
+ let isActive = plugin?.isCapturing ?? false
621
+ events([
622
+ "isActive": isActive,
623
+ "timestamp": Date().timeIntervalSince1970
624
+ ])
625
+ return nil
626
+ }
627
+
628
+ func onCancel(withArguments arguments: Any?) -> FlutterError? {
629
+ plugin?.statusEventSink = nil
630
+ return nil
631
+ }
632
+ }
633
+
634
+ // MARK: - System Decibel Stream Handler
635
+ @available(macOS 13.0, *)
636
+ class SystemDecibelStreamHandler: NSObject, FlutterStreamHandler {
637
+ weak var plugin: SystemCapturePlugin?
638
+
639
+ init(plugin: SystemCapturePlugin) {
640
+ self.plugin = plugin
641
+ }
642
+
643
+ func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
644
+ plugin?.decibelEventSink = events
645
+ // Update StreamOutput with decibel sink
646
+ plugin?.streamOutput?.decibelEventSink = events
647
+ return nil
648
+ }
649
+
650
+ func onCancel(withArguments arguments: Any?) -> FlutterError? {
651
+ plugin?.decibelEventSink = nil
652
+ plugin?.streamOutput?.decibelEventSink = nil
653
+ return nil
654
+ }
655
+ }
@@ -0,0 +1,12 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>NSPrivacyTrackingDomains</key>
6
+ <array/>
7
+ <key>NSPrivacyCollectedDataTypes</key>
8
+ <array/>
9
+ <key>NSPrivacyTracking</key>
10
+ <false/>
11
+ </dict>
12
+ </plist>
@@ -0,0 +1,30 @@
1
+ #
2
+ # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
3
+ # Run `pod lib lint desktop_audio_capture.podspec` to validate before publishing.
4
+ #
5
+ Pod::Spec.new do |s|
6
+ s.name = 'desktop_audio_capture'
7
+ s.version = '0.0.1'
8
+ s.summary = 'A new Flutter plugin project.'
9
+ s.description = <<-DESC
10
+ A new Flutter plugin project.
11
+ DESC
12
+ s.homepage = 'http://example.com'
13
+ s.license = { :file => '../LICENSE' }
14
+ s.author = { 'Your Company' => 'email@example.com' }
15
+
16
+ s.source = { :path => '.' }
17
+ s.source_files = 'Classes/**/*'
18
+
19
+ # If your plugin requires a privacy manifest, for example if it collects user
20
+ # data, update the PrivacyInfo.xcprivacy file to describe your plugin's
21
+ # privacy impact, and then uncomment this line. For more information,
22
+ # see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files
23
+ # s.resource_bundles = {'desktop_audio_capture_privacy' => ['Resources/PrivacyInfo.xcprivacy']}
24
+
25
+ s.dependency 'FlutterMacOS'
26
+
27
+ s.platform = :osx, '13.0'
28
+ s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
29
+ s.swift_version = '5.0'
30
+ end