@legendapp/spark 0.0.1-next.2 → 0.0.1-next.4

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 (422) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +10 -4
  3. package/ai-codex.ts +1 -0
  4. package/ai.ts +1 -0
  5. package/app-documents.ts +1 -0
  6. package/camera.ts +13 -0
  7. package/config-plugin.d.cts +2 -0
  8. package/contracts.ts +2 -0
  9. package/diagnostics.ts +1 -0
  10. package/expo-config.d.cts +1 -0
  11. package/metro.cjs +4 -1
  12. package/metro.d.cts +12 -0
  13. package/native.cjs +3 -1
  14. package/native.d.cts +6 -0
  15. package/package.json +93 -15
  16. package/patches/camera/react-native-nitro-image.patch +320 -0
  17. package/patches/camera/react-native-nitro-modules.patch +50 -0
  18. package/patches/camera/react-native-vision-camera.patch +2662 -0
  19. package/patches/camera/upstream.json +22 -0
  20. package/patches/dependencies.json +8 -0
  21. package/patches/react-native-enriched-markdown@1.0.2.patch +8684 -0
  22. package/postinstall.cjs +1 -0
  23. package/schema.json +276 -58
  24. package/settings-observable.ts +1 -0
  25. package/settings-window.ts +1 -0
  26. package/shortcuts-commands.ts +1 -0
  27. package/shortcuts-keyboard.ts +3 -0
  28. package/ui-classnames.ts +1 -0
  29. package/ui-glass.ts +1 -0
  30. package/ui-search.ts +1 -0
  31. package/ui-sidebar.ts +1 -0
  32. package/ui-split-view.ts +1 -0
  33. package/ui-swipe.ts +1 -0
  34. package/ui-symbol.ts +1 -0
  35. package/vendor/node_modules/@legendapp/spark-ai/package.json +19 -0
  36. package/vendor/node_modules/@legendapp/spark-ai/src/availability.ts +17 -0
  37. package/vendor/node_modules/@legendapp/spark-ai/src/index.ts +15 -0
  38. package/vendor/node_modules/@legendapp/spark-ai/src/invocations.ts +34 -0
  39. package/vendor/node_modules/@legendapp/spark-ai/src/output.ts +62 -0
  40. package/vendor/node_modules/@legendapp/spark-ai/src/run.ts +12 -0
  41. package/vendor/node_modules/@legendapp/spark-ai/src/types.ts +16 -0
  42. package/vendor/node_modules/@legendapp/spark-audio/macos/RNSparkAudio.h +2 -1
  43. package/vendor/node_modules/@legendapp/spark-audio/macos/RNSparkAudio.mm +126 -24
  44. package/vendor/node_modules/@legendapp/spark-audio/package.json +5 -2
  45. package/vendor/node_modules/@legendapp/spark-audio/src/NativeSparkAudio.ts +5 -1
  46. package/vendor/node_modules/@legendapp/spark-audio/src/desktop.ts +36 -32
  47. package/vendor/node_modules/@legendapp/spark-audio/src/events.ts +11 -0
  48. package/vendor/node_modules/@legendapp/spark-audio/src/hooks.ts +65 -0
  49. package/vendor/node_modules/@legendapp/spark-audio/src/index.web.ts +92 -33
  50. package/vendor/node_modules/@legendapp/spark-audio/src/media-session-unavailable.ts +6 -4
  51. package/vendor/node_modules/@legendapp/spark-audio/src/media-session.ts +52 -19
  52. package/vendor/node_modules/@legendapp/spark-audio/src/media-session.web.ts +18 -10
  53. package/vendor/node_modules/@legendapp/spark-audio/src/media-types.ts +40 -13
  54. package/vendor/node_modules/@legendapp/spark-audio/src/mobile.ts +40 -32
  55. package/vendor/node_modules/@legendapp/spark-audio/src/player.ts +97 -0
  56. package/vendor/node_modules/@legendapp/spark-audio/src/status-listeners.ts +21 -15
  57. package/vendor/node_modules/@legendapp/spark-audio/src/types.ts +9 -4
  58. package/vendor/node_modules/@legendapp/spark-audio/windows/SparkAudio/SparkAudio.h +67 -20
  59. package/vendor/node_modules/@legendapp/spark-auth-session/package.json +4 -3
  60. package/vendor/node_modules/@legendapp/spark-auth-session/src/core.ts +60 -42
  61. package/vendor/node_modules/@legendapp/spark-auth-session/src/index.ts +28 -12
  62. package/vendor/node_modules/@legendapp/spark-auth-session/src/mobile.ts +97 -41
  63. package/vendor/node_modules/@legendapp/spark-auth-session/src/types.ts +20 -9
  64. package/vendor/node_modules/@legendapp/spark-cli/dist/add-desktop.js +50 -7
  65. package/vendor/node_modules/@legendapp/spark-cli/dist/build-mode.js +1 -1
  66. package/vendor/node_modules/@legendapp/spark-cli/dist/build.js +145 -59
  67. package/vendor/node_modules/@legendapp/spark-cli/dist/cli-arguments.js +61 -0
  68. package/vendor/node_modules/@legendapp/spark-cli/dist/commands.js +10 -4
  69. package/vendor/node_modules/@legendapp/spark-cli/dist/create.js +37 -8
  70. package/vendor/node_modules/@legendapp/spark-cli/dist/credentials.js +86 -25
  71. package/vendor/node_modules/@legendapp/spark-cli/dist/dependency-pins.js +132 -0
  72. package/vendor/node_modules/@legendapp/spark-cli/dist/dev-command.js +13 -4
  73. package/vendor/node_modules/@legendapp/spark-cli/dist/dev.js +13 -10
  74. package/vendor/node_modules/@legendapp/spark-cli/dist/errors.js +100 -0
  75. package/vendor/node_modules/@legendapp/spark-cli/dist/examples.js +2 -2
  76. package/vendor/node_modules/@legendapp/spark-cli/dist/expo-dev-patch.cjs +4 -2
  77. package/vendor/node_modules/@legendapp/spark-cli/dist/expo-metro.cjs +1 -1
  78. package/vendor/node_modules/@legendapp/spark-cli/dist/expo-native.cjs +5 -1
  79. package/vendor/node_modules/@legendapp/spark-cli/dist/expo-node.js +3 -3
  80. package/vendor/node_modules/@legendapp/spark-cli/dist/helpers.js +1 -1
  81. package/vendor/node_modules/@legendapp/spark-cli/dist/index.js +52 -44
  82. package/vendor/node_modules/@legendapp/spark-cli/dist/json.js +16 -0
  83. package/vendor/node_modules/@legendapp/spark-cli/dist/local.js +1 -1
  84. package/vendor/node_modules/@legendapp/spark-cli/dist/metro.cjs +26 -5
  85. package/vendor/node_modules/@legendapp/spark-cli/dist/native-metadata.js +26 -0
  86. package/vendor/node_modules/@legendapp/spark-cli/dist/native-patch-preflight.js +86 -0
  87. package/vendor/node_modules/@legendapp/spark-cli/dist/native-preparation.js +88 -0
  88. package/vendor/node_modules/@legendapp/spark-cli/dist/notary-profiles.js +75 -0
  89. package/vendor/node_modules/@legendapp/spark-cli/dist/package.js +4 -2
  90. package/vendor/node_modules/@legendapp/spark-cli/dist/platform.js +16 -2
  91. package/vendor/node_modules/@legendapp/spark-cli/dist/process.js +7 -0
  92. package/vendor/node_modules/@legendapp/spark-cli/dist/project.js +90 -32
  93. package/vendor/node_modules/@legendapp/spark-cli/dist/release.js +86 -5
  94. package/vendor/node_modules/@legendapp/spark-cli/dist/release.json +23 -18
  95. package/vendor/node_modules/@legendapp/spark-cli/dist/runner-install.js +145 -31
  96. package/vendor/node_modules/@legendapp/spark-cli/dist/signing.js +4 -3
  97. package/vendor/node_modules/@legendapp/spark-cli/dist/universal.cjs +2 -1
  98. package/vendor/node_modules/@legendapp/spark-cli/dist/updates.js +41 -4
  99. package/vendor/node_modules/@legendapp/spark-cli/dist/windows.js +5 -9
  100. package/vendor/node_modules/@legendapp/spark-cli/package.json +7 -6
  101. package/vendor/node_modules/@legendapp/spark-cli/templates/blank-typescript/App.tsx +6 -12
  102. package/vendor/node_modules/@legendapp/spark-cli/templates/blank-typescript/desktop.config.json +5 -3
  103. package/vendor/node_modules/@legendapp/spark-cli/templates/blank-typescript/package.json +12 -10
  104. package/vendor/node_modules/@legendapp/spark-cli/templates/diff-lite/Git.macos.tsx +2 -2
  105. package/vendor/node_modules/@legendapp/spark-cli/templates/document-editor/Integration.macos.tsx +23 -21
  106. package/vendor/node_modules/@legendapp/spark-cli/templates/document-editor/Integration.windows.tsx +23 -21
  107. package/vendor/node_modules/@legendapp/spark-cli/templates/document-editor/io.desktop.ts +11 -8
  108. package/vendor/node_modules/@legendapp/spark-cli/templates/document-editor/io.macos.ts +3 -3
  109. package/vendor/node_modules/@legendapp/spark-cli/templates/example-shared/Lifecycle.desktop.tsx +8 -14
  110. package/vendor/node_modules/@legendapp/spark-cli/templates/example-shared/OpenFiles.desktop.tsx +5 -10
  111. package/vendor/node_modules/@legendapp/spark-cli/templates/example-shared/io.desktop.ts +11 -8
  112. package/vendor/node_modules/@legendapp/spark-cli/templates/example-shared/io.macos.ts +3 -3
  113. package/vendor/node_modules/@legendapp/spark-cli/templates/music-lite/assets.desktop.ts +2 -2
  114. package/vendor/node_modules/@legendapp/spark-cli/templates/notes-lite/Activation.desktop.tsx +2 -2
  115. package/vendor/node_modules/@legendapp/spark-cli/templates/notes-lite/Desktop.desktop.ts +37 -15
  116. package/vendor/node_modules/@legendapp/spark-cli/templates/notes-lite/session.ts +5 -6
  117. package/vendor/node_modules/@legendapp/spark-cli/templates/notes-lite/window-session.ts +5 -4
  118. package/vendor/node_modules/@legendapp/spark-cli/templates/universal/desktop.config.json +5 -3
  119. package/vendor/node_modules/@legendapp/spark-cli/templates/universal/metro.config.js +1 -1
  120. package/vendor/node_modules/@legendapp/spark-cli/templates/universal/package.json +14 -12
  121. package/vendor/node_modules/@legendapp/spark-cli/templates/universal/react-native.config.js +1 -1
  122. package/vendor/node_modules/@legendapp/spark-cli/templates/windows/package.json +14 -12
  123. package/vendor/node_modules/@legendapp/spark-cli/templates/windows/react-native.config.js +1 -1
  124. package/vendor/node_modules/@legendapp/spark-clipboard/macos/RNDesktopClipboard.mm +10 -16
  125. package/vendor/node_modules/@legendapp/spark-clipboard/package.json +2 -2
  126. package/vendor/node_modules/@legendapp/spark-clipboard/src/NativeDesktopClipboard.ts +1 -1
  127. package/vendor/node_modules/@legendapp/spark-clipboard/src/desktop.ts +55 -31
  128. package/vendor/node_modules/@legendapp/spark-clipboard/src/expo.ts +21 -11
  129. package/vendor/node_modules/@legendapp/spark-clipboard/src/options.ts +8 -0
  130. package/vendor/node_modules/@legendapp/spark-clipboard/src/types.ts +10 -0
  131. package/vendor/node_modules/@legendapp/spark-clipboard/windows/SparkClipboard/SparkClipboard.h +38 -15
  132. package/vendor/node_modules/@legendapp/spark-clipboard/windows/SparkClipboard/SparkClipboard.vcxproj +2 -0
  133. package/vendor/node_modules/@legendapp/spark-codex/RNCodex.podspec +22 -0
  134. package/vendor/node_modules/@legendapp/spark-codex/cpp/HybridCodexAppServer.hpp +23 -0
  135. package/vendor/node_modules/@legendapp/spark-codex/cpp/HybridCodexAppServer.mm +1529 -0
  136. package/vendor/node_modules/@legendapp/spark-codex/nitro.json +21 -0
  137. package/vendor/node_modules/@legendapp/spark-codex/nitrogen/generated/.gitattributes +1 -0
  138. package/vendor/node_modules/@legendapp/spark-codex/nitrogen/generated/ios/RNCodex+autolinking.rb +62 -0
  139. package/vendor/node_modules/@legendapp/spark-codex/nitrogen/generated/ios/RNCodex-Swift-Cxx-Bridge.cpp +17 -0
  140. package/vendor/node_modules/@legendapp/spark-codex/nitrogen/generated/ios/RNCodex-Swift-Cxx-Bridge.hpp +27 -0
  141. package/vendor/node_modules/@legendapp/spark-codex/nitrogen/generated/ios/RNCodex-Swift-Cxx-Umbrella.hpp +38 -0
  142. package/vendor/node_modules/@legendapp/spark-codex/nitrogen/generated/ios/RNCodexAutolinking.mm +35 -0
  143. package/vendor/node_modules/@legendapp/spark-codex/nitrogen/generated/ios/RNCodexAutolinking.swift +16 -0
  144. package/vendor/node_modules/@legendapp/spark-codex/nitrogen/generated/shared/c++/CodexAvailability.hpp +95 -0
  145. package/vendor/node_modules/@legendapp/spark-codex/nitrogen/generated/shared/c++/CodexRunResult.hpp +99 -0
  146. package/vendor/node_modules/@legendapp/spark-codex/nitrogen/generated/shared/c++/HybridCodexAppServerSpec.cpp +24 -0
  147. package/vendor/node_modules/@legendapp/spark-codex/nitrogen/generated/shared/c++/HybridCodexAppServerSpec.hpp +71 -0
  148. package/vendor/node_modules/@legendapp/spark-codex/package.json +35 -0
  149. package/vendor/node_modules/@legendapp/spark-codex/react-native.config.js +11 -0
  150. package/vendor/node_modules/@legendapp/spark-codex/src/Codex.nitro.ts +30 -0
  151. package/vendor/node_modules/@legendapp/spark-codex/src/index.ts +65 -0
  152. package/vendor/node_modules/@legendapp/spark-codex/src/native.ts +6 -0
  153. package/vendor/node_modules/@legendapp/spark-commands/package.json +21 -0
  154. package/vendor/node_modules/@legendapp/spark-commands/src/bindings.ts +90 -0
  155. package/vendor/node_modules/@legendapp/spark-commands/src/index.tsx +400 -0
  156. package/vendor/node_modules/@legendapp/spark-commands/src/router.ts +138 -0
  157. package/vendor/node_modules/@legendapp/spark-commands/src/storage.ts +20 -0
  158. package/vendor/node_modules/@legendapp/spark-context-menu/macos/RNContextMenu.mm +7 -8
  159. package/vendor/node_modules/@legendapp/spark-context-menu/package.json +4 -3
  160. package/vendor/node_modules/@legendapp/spark-context-menu/src/NativeContextMenu.ts +1 -1
  161. package/vendor/node_modules/@legendapp/spark-context-menu/src/index.ts +26 -26
  162. package/vendor/node_modules/@legendapp/spark-context-menu/windows/SparkContextMenu/SparkContextMenu.h +3 -2
  163. package/vendor/node_modules/@legendapp/spark-desktop-app/RNDesktopApp.podspec +2 -1
  164. package/vendor/node_modules/@legendapp/spark-desktop-app/cpp/SparkBinaryJSI.hpp +89 -0
  165. package/vendor/node_modules/@legendapp/spark-desktop-app/cpp/SparkBinaryWindows.hpp +22 -0
  166. package/vendor/node_modules/@legendapp/spark-desktop-app/macos/RNDesktopApp.mm +13 -7
  167. package/vendor/node_modules/@legendapp/spark-desktop-app/macos/SparkBinaryJSI.h +48 -0
  168. package/vendor/node_modules/@legendapp/spark-desktop-app/macos/SparkDesktop.h +4 -0
  169. package/vendor/node_modules/@legendapp/spark-desktop-app/macos/SparkDesktop.mm +33 -33
  170. package/vendor/node_modules/@legendapp/spark-desktop-app/macos/SparkDesktopError.h +20 -0
  171. package/vendor/node_modules/@legendapp/spark-desktop-app/macos/SparkLifecycle.h +26 -0
  172. package/vendor/node_modules/@legendapp/spark-desktop-app/macos/SparkLifecycle.mm +49 -0
  173. package/vendor/node_modules/@legendapp/spark-desktop-app/macos/SparkQuitCoordinator.h +9 -0
  174. package/vendor/node_modules/@legendapp/spark-desktop-app/macos/SparkQuitCoordinator.mm +70 -0
  175. package/vendor/node_modules/@legendapp/spark-desktop-app/macos/SparkWindow.mm +9 -6
  176. package/vendor/node_modules/@legendapp/spark-desktop-app/macos/recent-documents/RNRecentDocumentController.mm +55 -0
  177. package/vendor/node_modules/@legendapp/spark-desktop-app/package.json +5 -3
  178. package/vendor/node_modules/@legendapp/spark-desktop-app/src/NativeDesktopApp.ts +1 -1
  179. package/vendor/node_modules/@legendapp/spark-desktop-app/src/api.ts +86 -40
  180. package/vendor/node_modules/@legendapp/spark-desktop-app/src/contracts/accelerator.ts +40 -0
  181. package/vendor/node_modules/@legendapp/spark-desktop-app/src/contracts/index.ts +56 -0
  182. package/vendor/node_modules/@legendapp/spark-desktop-app/src/contracts/menu.ts +116 -0
  183. package/vendor/node_modules/@legendapp/spark-desktop-app/src/contracts/native-buffer.ts +22 -0
  184. package/vendor/node_modules/@legendapp/spark-desktop-app/src/contracts/path.ts +24 -0
  185. package/vendor/node_modules/@legendapp/spark-desktop-app/src/events.ts +75 -0
  186. package/vendor/node_modules/@legendapp/spark-desktop-app/src/index.ts +2 -0
  187. package/vendor/node_modules/@legendapp/spark-desktop-app/src/index.windows.ts +2 -0
  188. package/vendor/node_modules/@legendapp/spark-desktop-app/src/open-requests.ts +43 -0
  189. package/vendor/node_modules/@legendapp/spark-desktop-app/src/restoration.ts +11 -0
  190. package/vendor/node_modules/@legendapp/spark-desktop-app/src/transport.ts +10 -0
  191. package/vendor/node_modules/@legendapp/spark-desktop-config/app.plugin.cjs +10 -5
  192. package/vendor/node_modules/@legendapp/spark-desktop-config/app.plugin.d.cts +3 -0
  193. package/vendor/node_modules/@legendapp/spark-desktop-config/autolinking.cjs +35 -0
  194. package/vendor/node_modules/@legendapp/spark-desktop-config/config.cjs +18 -3
  195. package/vendor/node_modules/@legendapp/spark-desktop-config/config.d.cts +89 -9
  196. package/vendor/node_modules/@legendapp/spark-desktop-config/expo.cjs +1 -0
  197. package/vendor/node_modules/@legendapp/spark-desktop-config/expo.d.cts +3 -0
  198. package/vendor/node_modules/@legendapp/spark-desktop-config/identity.cjs +3 -2
  199. package/vendor/node_modules/@legendapp/spark-desktop-config/lifecycle.cjs +20 -0
  200. package/vendor/node_modules/@legendapp/spark-desktop-config/native-patches.cjs +106 -0
  201. package/vendor/node_modules/@legendapp/spark-desktop-config/native-patches.d.cts +1 -0
  202. package/vendor/node_modules/@legendapp/spark-desktop-config/package.json +7 -5
  203. package/vendor/node_modules/@legendapp/spark-desktop-config/schema.json +276 -58
  204. package/vendor/node_modules/@legendapp/spark-desktop-config/updates.cjs +1 -0
  205. package/vendor/node_modules/@legendapp/spark-desktop-config/validation.cjs +35 -0
  206. package/vendor/node_modules/@legendapp/spark-desktop-config/windows.plugin.cjs +2 -1
  207. package/vendor/node_modules/@legendapp/spark-desktop-host/AppDelegate.mm +129 -11
  208. package/vendor/node_modules/@legendapp/spark-desktop-host/package.json +2 -2
  209. package/vendor/node_modules/@legendapp/spark-desktop-host/windows/application.inc +277 -121
  210. package/vendor/node_modules/@legendapp/spark-desktop-links/common/AuthLoopback.h +5 -7
  211. package/vendor/node_modules/@legendapp/spark-desktop-links/macos/RNDesktopLinks.mm +10 -9
  212. package/vendor/node_modules/@legendapp/spark-desktop-links/package.json +2 -2
  213. package/vendor/node_modules/@legendapp/spark-desktop-links/src/NativeDesktopLinks.ts +1 -1
  214. package/vendor/node_modules/@legendapp/spark-desktop-links/src/api.ts +27 -33
  215. package/vendor/node_modules/@legendapp/spark-desktop-links/src/expo.ts +3 -4
  216. package/vendor/node_modules/@legendapp/spark-desktop-links/src/native.ts +9 -0
  217. package/vendor/node_modules/@legendapp/spark-desktop-links/windows/SparkLinks/SparkLinks.h +22 -22
  218. package/vendor/node_modules/@legendapp/spark-desktop-shortcuts/RNDesktopShortcuts.podspec +1 -0
  219. package/vendor/node_modules/@legendapp/spark-desktop-shortcuts/macos/RNDesktopShortcuts.mm +1 -1
  220. package/vendor/node_modules/@legendapp/spark-desktop-shortcuts/macos/keyboard-manager/RNKeyboardManager.h +9 -0
  221. package/vendor/node_modules/@legendapp/spark-desktop-shortcuts/macos/keyboard-manager/RNKeyboardManager.mm +307 -0
  222. package/vendor/node_modules/@legendapp/spark-desktop-shortcuts/package.json +8 -5
  223. package/vendor/node_modules/@legendapp/spark-desktop-shortcuts/src/NativeDesktopShortcuts.ts +1 -1
  224. package/vendor/node_modules/@legendapp/spark-desktop-shortcuts/src/api.ts +39 -14
  225. package/vendor/node_modules/@legendapp/spark-desktop-shortcuts/src/keyboard-manager/NativeKeyboardManager.ts +12 -0
  226. package/vendor/node_modules/@legendapp/spark-desktop-shortcuts/src/keyboard-manager/codes.ts +108 -0
  227. package/vendor/node_modules/@legendapp/spark-desktop-shortcuts/src/keyboard-manager/index.ts +99 -0
  228. package/vendor/node_modules/@legendapp/spark-desktop-windows/RNDesktopWindows.podspec +2 -0
  229. package/vendor/node_modules/@legendapp/spark-desktop-windows/macos/RNDesktopWindows.mm +229 -103
  230. package/vendor/node_modules/@legendapp/spark-desktop-windows/macos/SparkWindowGeometry.h +63 -0
  231. package/vendor/node_modules/@legendapp/spark-desktop-windows/macos/window-manager/LegendWindowRegistry.h +20 -0
  232. package/vendor/node_modules/@legendapp/spark-desktop-windows/macos/window-manager/LegendWindowRegistry.mm +27 -0
  233. package/vendor/node_modules/@legendapp/spark-desktop-windows/macos/window-manager/RNWindowManager.h +12 -0
  234. package/vendor/node_modules/@legendapp/spark-desktop-windows/macos/window-manager/RNWindowManager.mm +3169 -0
  235. package/vendor/node_modules/@legendapp/spark-desktop-windows/macos/window-manager/RNWindowManagerStartup.mm +31 -0
  236. package/vendor/node_modules/@legendapp/spark-desktop-windows/macos/window-manager/SidebarStartupFallbacks.mm +14 -0
  237. package/vendor/node_modules/@legendapp/spark-desktop-windows/package.json +10 -6
  238. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/NativeDesktopWindowManager.ts +1 -1
  239. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/api.ts +146 -70
  240. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/diagnostics.ts +17 -0
  241. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/index.ts +3 -1
  242. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/index.windows.ts +3 -1
  243. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/macos-adapter.ts +53 -0
  244. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/macos.ts +51 -0
  245. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/transport.ts +41 -0
  246. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/types.ts +140 -0
  247. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/validation.ts +162 -0
  248. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/window-manager/NativeWindowManager.ts +28 -0
  249. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/windows/WindowProvider.tsx +20 -0
  250. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/windows/createWindowsNavigator.tsx +78 -0
  251. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/windows/index.ts +6 -0
  252. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/windows/primaryWindowLifecycle.ts +34 -0
  253. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/windows/types.ts +11 -0
  254. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/windows/usePrimaryWindowLifecycle.ts +18 -0
  255. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/windows/useWindowFocusEffect.ts +17 -0
  256. package/vendor/node_modules/@legendapp/spark-documents/package.json +22 -0
  257. package/vendor/node_modules/@legendapp/spark-documents/src/controller.ts +97 -0
  258. package/vendor/node_modules/@legendapp/spark-documents/src/documentTransition.ts +13 -0
  259. package/vendor/node_modules/@legendapp/spark-documents/src/hooks.ts +139 -0
  260. package/vendor/node_modules/@legendapp/spark-documents/src/index.tsx +50 -0
  261. package/vendor/node_modules/@legendapp/spark-documents/src/reload.ts +39 -0
  262. package/vendor/node_modules/@legendapp/spark-documents/src/requests.ts +15 -0
  263. package/vendor/node_modules/@legendapp/spark-drag-drop/package.json +4 -3
  264. package/vendor/node_modules/@legendapp/spark-drag-drop/src/DesktopDragViewNativeComponent.ts +1 -1
  265. package/vendor/node_modules/@legendapp/spark-drag-drop/src/contracts.ts +73 -18
  266. package/vendor/node_modules/@legendapp/spark-drag-drop/src/index.tsx +58 -15
  267. package/vendor/node_modules/@legendapp/spark-drag-drop/windows/SparkDragDrop/SparkDragDrop.h +14 -8
  268. package/vendor/node_modules/@legendapp/spark-file-dialog/ios/RNFileDialog.mm +34 -121
  269. package/vendor/node_modules/@legendapp/spark-file-dialog/package.json +12 -5
  270. package/vendor/node_modules/@legendapp/spark-file-dialog/src/NativeFileDialog.ts +1 -5
  271. package/vendor/node_modules/@legendapp/spark-file-dialog/src/index.ts +19 -84
  272. package/vendor/node_modules/@legendapp/spark-file-dialog/src/native.ts +12 -0
  273. package/vendor/node_modules/@legendapp/spark-file-dialog/src/options.ts +34 -14
  274. package/vendor/node_modules/@legendapp/spark-file-dialog/src/types.ts +26 -0
  275. package/vendor/node_modules/@legendapp/spark-file-dialog/windows/SparkFileDialog/SparkFileDialog.h +42 -58
  276. package/vendor/node_modules/@legendapp/spark-file-system/RNDesktopFileSystem.podspec +1 -0
  277. package/vendor/node_modules/@legendapp/spark-file-system/macos/RNDesktopFileSystem.mm +32 -21
  278. package/vendor/node_modules/@legendapp/spark-file-system/macos/SparkFileMutations.h +24 -0
  279. package/vendor/node_modules/@legendapp/spark-file-system/macos/SparkFileMutations.mm +215 -0
  280. package/vendor/node_modules/@legendapp/spark-file-system/macos/file-scanner/RNFileScanner.h +9 -0
  281. package/vendor/node_modules/@legendapp/spark-file-system/macos/file-scanner/RNFileScanner.mm +82 -0
  282. package/vendor/node_modules/@legendapp/spark-file-system/macos/file-scanner/RNFileScannerCore.h +47 -0
  283. package/vendor/node_modules/@legendapp/spark-file-system/macos/file-scanner/RNFileScannerCore.mm +306 -0
  284. package/vendor/node_modules/@legendapp/spark-file-system/package.json +8 -6
  285. package/vendor/node_modules/@legendapp/spark-file-system/src/NativeDesktopFileSystem.ts +1 -1
  286. package/vendor/node_modules/@legendapp/spark-file-system/src/file-scanner/NativeFileScanner.ts +11 -0
  287. package/vendor/node_modules/@legendapp/spark-file-system/src/file-scanner/index.ts +93 -0
  288. package/vendor/node_modules/@legendapp/spark-file-system/src/handles.ts +48 -14
  289. package/vendor/node_modules/@legendapp/spark-file-system/src/index.ts +134 -45
  290. package/vendor/node_modules/@legendapp/spark-file-system/src/path.ts +1 -9
  291. package/vendor/node_modules/@legendapp/spark-file-system/windows/SparkFileSystem/SparkFileSystem.h +102 -26
  292. package/vendor/node_modules/@legendapp/spark-file-system/windows/SparkFileSystem/SparkFileSystem.vcxproj +2 -0
  293. package/vendor/node_modules/@legendapp/spark-global-shortcuts/RNDesktopGlobalShortcuts.podspec +1 -0
  294. package/vendor/node_modules/@legendapp/spark-global-shortcuts/macos/RNDesktopGlobalShortcuts.mm +7 -4
  295. package/vendor/node_modules/@legendapp/spark-global-shortcuts/package.json +4 -3
  296. package/vendor/node_modules/@legendapp/spark-global-shortcuts/src/NativeDesktopGlobalShortcuts.ts +1 -1
  297. package/vendor/node_modules/@legendapp/spark-global-shortcuts/src/index.ts +39 -10
  298. package/vendor/node_modules/@legendapp/spark-global-shortcuts/windows/SparkGlobalShortcuts/SparkGlobalShortcuts.h +5 -4
  299. package/vendor/node_modules/@legendapp/spark-message-dialog/macos/RNDesktopMessageDialog.mm +2 -2
  300. package/vendor/node_modules/@legendapp/spark-message-dialog/package.json +2 -2
  301. package/vendor/node_modules/@legendapp/spark-message-dialog/src/NativeDesktopMessageDialog.ts +1 -1
  302. package/vendor/node_modules/@legendapp/spark-message-dialog/src/index.ts +48 -11
  303. package/vendor/node_modules/@legendapp/spark-native-menu/ios/RNNativeMenu.mm +141 -609
  304. package/vendor/node_modules/@legendapp/spark-native-menu/package.json +9 -4
  305. package/vendor/node_modules/@legendapp/spark-native-menu/src/NativeMenu.ts +2 -7
  306. package/vendor/node_modules/@legendapp/spark-native-menu/src/api.ts +76 -121
  307. package/vendor/node_modules/@legendapp/spark-native-menu/src/hooks.ts +97 -0
  308. package/vendor/node_modules/@legendapp/spark-native-menu/src/windows-menus.ts +43 -37
  309. package/vendor/node_modules/@legendapp/spark-notifications/macos/RNDesktopNotifications.mm +75 -30
  310. package/vendor/node_modules/@legendapp/spark-notifications/package.json +2 -2
  311. package/vendor/node_modules/@legendapp/spark-notifications/src/NativeDesktopNotifications.ts +1 -1
  312. package/vendor/node_modules/@legendapp/spark-notifications/src/index.ts +130 -42
  313. package/vendor/node_modules/@legendapp/spark-notifications/windows/SparkNotifications/SparkNotifications.h +51 -8
  314. package/vendor/node_modules/@legendapp/spark-processes/macos/RNDesktopProcesses.mm +94 -21
  315. package/vendor/node_modules/@legendapp/spark-processes/package.json +4 -3
  316. package/vendor/node_modules/@legendapp/spark-processes/src/NativeDesktopProcesses.ts +1 -1
  317. package/vendor/node_modules/@legendapp/spark-processes/src/index.ts +124 -28
  318. package/vendor/node_modules/@legendapp/spark-processes/src/types.ts +34 -0
  319. package/vendor/node_modules/@legendapp/spark-processes/windows/SparkProcesses/SparkProcesses.h +83 -34
  320. package/vendor/node_modules/@legendapp/spark-processes/windows/SparkProcesses/SparkProcesses.vcxproj +2 -0
  321. package/vendor/node_modules/@legendapp/spark-secure-storage/macos/RNDesktopSecureStorage.mm +49 -40
  322. package/vendor/node_modules/@legendapp/spark-secure-storage/package.json +2 -2
  323. package/vendor/node_modules/@legendapp/spark-secure-storage/src/NativeDesktopSecureStorage.ts +1 -1
  324. package/vendor/node_modules/@legendapp/spark-secure-storage/src/desktop.ts +16 -14
  325. package/vendor/node_modules/@legendapp/spark-secure-storage/src/expo.ts +0 -1
  326. package/vendor/node_modules/@legendapp/spark-secure-storage/src/options.ts +3 -2
  327. package/vendor/node_modules/@legendapp/spark-secure-storage/src/unavailable.ts +6 -3
  328. package/vendor/node_modules/@legendapp/spark-settings/package.json +10 -3
  329. package/vendor/node_modules/@legendapp/spark-settings/src/fileStorage.ts +11 -0
  330. package/vendor/node_modules/@legendapp/spark-settings/src/index.ts +7 -9
  331. package/vendor/node_modules/@legendapp/spark-settings/src/storage/index.ts +142 -0
  332. package/vendor/node_modules/@legendapp/spark-settings/src/store.ts +63 -21
  333. package/vendor/node_modules/@legendapp/spark-settings-ui/package.json +20 -0
  334. package/vendor/node_modules/@legendapp/spark-settings-ui/src/index.tsx +436 -0
  335. package/vendor/node_modules/@legendapp/spark-settings-ui/src/options.ts +10 -0
  336. package/vendor/node_modules/@legendapp/spark-sqlite/package.json +3 -3
  337. package/vendor/node_modules/@legendapp/spark-sqlite/src/database.ts +182 -0
  338. package/vendor/node_modules/@legendapp/spark-sqlite/src/index.ts +19 -6
  339. package/vendor/node_modules/@legendapp/spark-sqlite/src/types.ts +22 -0
  340. package/vendor/node_modules/@legendapp/spark-system/macos/RNDesktopSystem.mm +36 -11
  341. package/vendor/node_modules/@legendapp/spark-system/package.json +2 -2
  342. package/vendor/node_modules/@legendapp/spark-system/src/NativeDesktopSystem.ts +1 -1
  343. package/vendor/node_modules/@legendapp/spark-system/src/index.ts +89 -32
  344. package/vendor/node_modules/@legendapp/spark-system/windows/SparkSystem/SparkSystem.h +8 -4
  345. package/vendor/node_modules/@legendapp/spark-tray/macos/RNDesktopTray.mm +20 -8
  346. package/vendor/node_modules/@legendapp/spark-tray/package.json +2 -2
  347. package/vendor/node_modules/@legendapp/spark-tray/src/NativeDesktopTray.ts +1 -1
  348. package/vendor/node_modules/@legendapp/spark-tray/src/index.ts +91 -34
  349. package/vendor/node_modules/@legendapp/spark-tray/windows/SparkTray/SparkTray.h +45 -15
  350. package/vendor/node_modules/@legendapp/spark-ui/macos/RNSparkButton.mm +2 -1
  351. package/vendor/node_modules/@legendapp/spark-ui/macos/RNSparkTextInput.mm +10 -2
  352. package/vendor/node_modules/@legendapp/spark-ui/macos/appkit-split-view/RNSidebarSplitViewComponent.h +15 -0
  353. package/vendor/node_modules/@legendapp/spark-ui/macos/appkit-split-view/RNSidebarSplitViewComponent.mm +1419 -0
  354. package/vendor/node_modules/@legendapp/spark-ui/macos/glass-effect-view/RNGlassEffectView.mm +72 -0
  355. package/vendor/node_modules/@legendapp/spark-ui/macos/native-select/RNNativeSegmentedControl.h +15 -0
  356. package/vendor/node_modules/@legendapp/spark-ui/macos/native-select/RNNativeSegmentedControl.mm +194 -0
  357. package/vendor/node_modules/@legendapp/spark-ui/macos/native-select/RNNativeSelect.h +15 -0
  358. package/vendor/node_modules/@legendapp/spark-ui/macos/native-select/RNNativeSelect.mm +180 -0
  359. package/vendor/node_modules/@legendapp/spark-ui/macos/sf-symbol/RNSFSymbol.h +15 -0
  360. package/vendor/node_modules/@legendapp/spark-ui/macos/sf-symbol/RNSFSymbol.mm +155 -0
  361. package/vendor/node_modules/@legendapp/spark-ui/macos/sidebar/RNSidebar.h +18 -0
  362. package/vendor/node_modules/@legendapp/spark-ui/macos/sidebar/RNSidebar.mm +645 -0
  363. package/vendor/node_modules/@legendapp/spark-ui/macos/swipe-actions/RNSwipeActions.h +10 -0
  364. package/vendor/node_modules/@legendapp/spark-ui/macos/swipe-actions/RNSwipeActions.mm +422 -0
  365. package/vendor/node_modules/@legendapp/spark-ui/macos/text-input-search/RNTextInputSearch.h +15 -0
  366. package/vendor/node_modules/@legendapp/spark-ui/macos/text-input-search/RNTextInputSearch.mm +164 -0
  367. package/vendor/node_modules/@legendapp/spark-ui/package.json +17 -4
  368. package/vendor/node_modules/@legendapp/spark-ui/src/SparkSelectNativeComponent.ts +3 -1
  369. package/vendor/node_modules/@legendapp/spark-ui/src/SparkTextInputNativeComponent.ts +6 -2
  370. package/vendor/node_modules/@legendapp/spark-ui/src/appkit-split-view/SidebarSplitViewNativeComponent.ts +37 -0
  371. package/vendor/node_modules/@legendapp/spark-ui/src/appkit-split-view/index.tsx +86 -0
  372. package/vendor/node_modules/@legendapp/spark-ui/src/availability.ts +10 -0
  373. package/vendor/node_modules/@legendapp/spark-ui/src/classnames/index.ts +8 -0
  374. package/vendor/node_modules/@legendapp/spark-ui/src/control.ts +42 -0
  375. package/vendor/node_modules/@legendapp/spark-ui/src/desktop.tsx +56 -0
  376. package/vendor/node_modules/@legendapp/spark-ui/src/glass-effect-view/index.tsx +15 -0
  377. package/vendor/node_modules/@legendapp/spark-ui/src/index.android.tsx +35 -21
  378. package/vendor/node_modules/@legendapp/spark-ui/src/index.ios.tsx +55 -26
  379. package/vendor/node_modules/@legendapp/spark-ui/src/index.tsx +1 -27
  380. package/vendor/node_modules/@legendapp/spark-ui/src/index.web.tsx +45 -14
  381. package/vendor/node_modules/@legendapp/spark-ui/src/index.windows.tsx +1 -42
  382. package/vendor/node_modules/@legendapp/spark-ui/src/mobile-input.tsx +11 -0
  383. package/vendor/node_modules/@legendapp/spark-ui/src/native-select/NativeSegmentedControlNativeComponent.ts +17 -0
  384. package/vendor/node_modules/@legendapp/spark-ui/src/native-select/NativeSelectNativeComponent.ts +17 -0
  385. package/vendor/node_modules/@legendapp/spark-ui/src/select.ts +5 -3
  386. package/vendor/node_modules/@legendapp/spark-ui/src/sf-symbol/SFSymbolNativeComponent.ts +13 -0
  387. package/vendor/node_modules/@legendapp/spark-ui/src/sf-symbol/index.tsx +27 -0
  388. package/vendor/node_modules/@legendapp/spark-ui/src/sidebar/SidebarItemNativeComponent.ts +25 -0
  389. package/vendor/node_modules/@legendapp/spark-ui/src/sidebar/SidebarNativeComponent.ts +24 -0
  390. package/vendor/node_modules/@legendapp/spark-ui/src/sidebar/index.tsx +104 -0
  391. package/vendor/node_modules/@legendapp/spark-ui/src/specialized.ts +22 -0
  392. package/vendor/node_modules/@legendapp/spark-ui/src/swipe-actions/SwipeActionsNativeComponent.ts +11 -0
  393. package/vendor/node_modules/@legendapp/spark-ui/src/swipe-actions/index.tsx +41 -0
  394. package/vendor/node_modules/@legendapp/spark-ui/src/text-input-search/TextInputSearchNativeComponent.ts +33 -0
  395. package/vendor/node_modules/@legendapp/spark-ui/src/text-input-search/index.tsx +34 -0
  396. package/vendor/node_modules/@legendapp/spark-ui/src/types.ts +30 -19
  397. package/vendor/node_modules/@legendapp/spark-ui/uniwind.ts +3 -1
  398. package/vendor/node_modules/@legendapp/spark-ui/uniwind.web.ts +3 -1
  399. package/vendor/node_modules/@legendapp/spark-ui/windows/SparkUI/SparkUI.h +16 -11
  400. package/vendor/node_modules/@legendapp/spark-updates/macos/RNDesktopUpdates.mm +21 -8
  401. package/vendor/node_modules/@legendapp/spark-updates/package.json +2 -2
  402. package/vendor/node_modules/@legendapp/spark-updates/src/NativeDesktopUpdates.ts +3 -2
  403. package/vendor/node_modules/@legendapp/spark-updates/src/index.ts +71 -15
  404. package/vendor/node_modules/@legendapp/spark-webview/package.json +2 -2
  405. package/vendor/node_modules/@legendapp/spark-webview/src/WebView.tsx +11 -0
  406. package/vendor/node_modules/@legendapp/spark-webview/src/adapter.ts +55 -0
  407. package/vendor/node_modules/@legendapp/spark-webview/src/index.ts +2 -2
  408. package/vendor/node_modules/@legendapp/spark-webview/src/types.ts +32 -0
  409. package/vendor/node_modules/@legendapp/spark-webview/src/view-props.ts +28 -0
  410. package/vendor/node_modules/@legendapp/spark-window-options/index.cjs +51 -14
  411. package/vendor/node_modules/@legendapp/spark-window-options/index.d.ts +21 -8
  412. package/vendor/node_modules/@legendapp/spark-window-options/package.json +1 -1
  413. package/windows-macos.ts +1 -0
  414. package/expo-metro.cjs +0 -1
  415. package/message-dialog.ts +0 -1
  416. package/universal.cjs +0 -1
  417. package/vendor/node_modules/@legendapp/spark-desktop-shortcuts/src/accelerator.ts +0 -22
  418. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/windows-options.ts +0 -11
  419. package/vendor/node_modules/@legendapp/spark-file-dialog/src/index.windows.ts +0 -15
  420. package/vendor/node_modules/@legendapp/spark-global-shortcuts/src/accelerator.ts +0 -22
  421. package/vendor/node_modules/@legendapp/spark-ui/macos/RNSparkSelect.h +0 -4
  422. package/vendor/node_modules/@legendapp/spark-ui/macos/RNSparkSelect.mm +0 -53
@@ -1,6 +1,52 @@
1
1
  #import "RNSparkAudio.h"
2
2
  #import <AVFoundation/AVFoundation.h>
3
3
  #import <MediaPlayer/MediaPlayer.h>
4
+ /** Native observation exists only while the corresponding public status stream is observed. */
5
+ @interface SparkAudioObservation : NSObject
6
+ @property AVPlayer *player;
7
+ @property id timeObserver;
8
+ @property id endObserver;
9
+ @property NSArray<NSString *> *keys;
10
+ @property (copy) void (^notify)(void);
11
+ @property BOOL stopped;
12
+ - (instancetype)initWithPlayer:(AVPlayer *)player notify:(void (^)(void))notify;
13
+ - (instancetype)initWithPlayer:(AVPlayer *)player positionUpdates:(BOOL)positionUpdates notify:(void (^)(void))notify;
14
+ - (void)remove;
15
+ @end
16
+ @implementation SparkAudioObservation
17
+ - (instancetype)initWithPlayer:(AVPlayer *)player notify:(void (^)(void))notify {
18
+ return [self initWithPlayer:player positionUpdates:YES notify:notify];
19
+ }
20
+ - (instancetype)initWithPlayer:(AVPlayer *)player positionUpdates:(BOOL)positionUpdates notify:(void (^)(void))notify {
21
+ if (self = [super init]) {
22
+ self.player = player; self.notify = notify;
23
+ self.keys = positionUpdates ? @[@"status", @"timeControlStatus", @"volume", @"currentItem.status", @"currentItem.duration"] : @[@"status", @"currentItem.status"];
24
+ for (NSString *key in self.keys)
25
+ [player addObserver:self forKeyPath:key options:NSKeyValueObservingOptionNew context:nil];
26
+ __weak SparkAudioObservation *weakSelf = self;
27
+ if (positionUpdates) self.timeObserver = [player addPeriodicTimeObserverForInterval:CMTimeMake(1, 4) queue:dispatch_get_main_queue() usingBlock:^(CMTime time) {
28
+ SparkAudioObservation *owner = weakSelf; if (owner && !owner.stopped) owner.notify();
29
+ }];
30
+ if (positionUpdates) self.endObserver = [NSNotificationCenter.defaultCenter addObserverForName:AVPlayerItemDidPlayToEndTimeNotification object:player.currentItem queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification *event) {
31
+ SparkAudioObservation *owner = weakSelf; if (owner && !owner.stopped) owner.notify();
32
+ }];
33
+ }
34
+ return self;
35
+ }
36
+ - (void)observeValueForKeyPath:(NSString *)key ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
37
+ if (NSThread.isMainThread) { if (!self.stopped) self.notify(); }
38
+ else { __weak SparkAudioObservation *weakSelf = self; dispatch_async(dispatch_get_main_queue(), ^{ SparkAudioObservation *owner = weakSelf; if (owner && !owner.stopped) owner.notify(); }); }
39
+ }
40
+ - (void)remove {
41
+ if (self.stopped) return;
42
+ self.stopped = YES;
43
+ for (NSString *key in self.keys) [self.player removeObserver:self forKeyPath:key];
44
+ if (self.timeObserver) [self.player removeTimeObserver:self.timeObserver];
45
+ if (self.endObserver) [NSNotificationCenter.defaultCenter removeObserver:self.endObserver];
46
+ self.timeObserver = nil; self.endObserver = nil; self.notify = nil;
47
+ }
48
+ - (void)dealloc { [self remove]; }
49
+ @end
4
50
  @interface RNSparkAudio ()
5
51
  @property NSMutableDictionary<NSString *, AVPlayer *> *players;
6
52
  @property NSString *activeID;
@@ -8,7 +54,11 @@
8
54
  @property NSMutableDictionary<NSString *, id> *targets;
9
55
  @property NSString *sessionID;
10
56
  @property NSMutableDictionary *session;
11
- @property NSMutableArray *commands;
57
+ @property NSMutableDictionary<NSString *, SparkAudioObservation *> *observations;
58
+ @property NSMutableDictionary<NSString *, NSDictionary *> *lastStatuses;
59
+ @property NSMutableDictionary<NSString *, id> *endObservers;
60
+ @property NSMutableDictionary<NSString *, SparkAudioObservation *> *readiness;
61
+ @property NSMutableDictionary *readyRejects;
12
62
  @property NSUInteger artworkGeneration;
13
63
  @end
14
64
  static NSDictionary *MediaInfo(NSDictionary *metadata) {
@@ -28,9 +78,12 @@ static NSDictionary<NSString *, MPRemoteCommand *> *RemoteCommands() {
28
78
  @implementation RNSparkAudio
29
79
  RCT_EXPORT_MODULE(NativeSparkAudio)
30
80
  + (BOOL)requiresMainQueueSetup { return YES; }
81
+ - (NSArray<NSString *> *)supportedEvents { return @[@"sparkMediaCommand", @"sparkAudioStatus"]; }
31
82
  - (instancetype)init { if (self = [super init]) {
32
83
  _players = [NSMutableDictionary new];
33
- _metadata = [NSMutableDictionary new]; _targets = [NSMutableDictionary new]; _commands = [NSMutableArray new];
84
+ _metadata = [NSMutableDictionary new]; _targets = [NSMutableDictionary new];
85
+ _observations = [NSMutableDictionary new]; _lastStatuses = [NSMutableDictionary new]; _endObservers = [NSMutableDictionary new];
86
+ _readiness = [NSMutableDictionary new]; _readyRejects = [NSMutableDictionary new];
34
87
  __weak RNSparkAudio *weakSelf = self;
35
88
  [RemoteCommands() enumerateKeysAndObjectsUsingBlock:^(NSString *name, MPRemoteCommand *command, BOOL *stop) {
36
89
  command.enabled = [@[@"play", @"pause", @"seekTo"] containsObject:name];
@@ -43,18 +96,41 @@ RCT_EXPORT_MODULE(NativeSparkAudio)
43
96
  if (![owner.session[@"commands"] containsObject:name]) return;
44
97
  NSMutableDictionary *value = [@{@"command": name} mutableCopy];
45
98
  if ([event isKindOfClass:MPChangePlaybackPositionCommandEvent.class]) value[@"position"] = @(((MPChangePlaybackPositionCommandEvent *)event).positionTime);
46
- if (owner.commands.count < 128) [owner.commands addObject:value];
99
+ [owner sendEventWithName:@"sparkMediaCommand" body:@{@"id": owner.sessionID, @"value": value}];
47
100
  } else {
48
101
  AVPlayer *player = owner.players[owner.activeID ?: @""];
49
102
  if ([name isEqual:@"play"]) [player play];
50
103
  else if ([name isEqual:@"pause"]) [player pause];
51
104
  else if ([event isKindOfClass:MPChangePlaybackPositionCommandEvent.class]) [player seekToTime:CMTimeMakeWithSeconds(((MPChangePlaybackPositionCommandEvent *)event).positionTime, 600)];
105
+ [owner publishPlayer:owner.activeID];
52
106
  }
53
107
  });
54
108
  return MPRemoteCommandHandlerStatusSuccess;
55
109
  }];
56
110
  }];
57
111
  } return self; }
112
+ - (NSDictionary *)statusForPlayer:(AVPlayer *)player {
113
+ double position = CMTimeGetSeconds(player.currentTime), duration = CMTimeGetSeconds(player.currentItem.duration);
114
+ if (!isfinite(position)) position = 0; if (!isfinite(duration)) duration = 0;
115
+ NSError *error = player.error ?: player.currentItem.error;
116
+ return @{ @"volume": @(player.volume), @"playing": @(player.timeControlStatus == AVPlayerTimeControlStatusPlaying), @"currentTime": @(position), @"duration": @(duration), @"didJustFinish": @(duration > 0 && position >= duration && player.rate == 0), @"error": error.localizedDescription ?: NSNull.null };
117
+ }
118
+ - (void)emitStatus:(NSString *)identifier {
119
+ AVPlayer *player = self.players[identifier]; if (!player || !self.observations[identifier]) return;
120
+ NSDictionary *status = [self statusForPlayer:player];
121
+ if ([self.lastStatuses[identifier] isEqual:status]) return;
122
+ self.lastStatuses[identifier] = status;
123
+ [self sendEventWithName:@"sparkAudioStatus" body:@{@"id": identifier, @"value": status}];
124
+ }
125
+ - (void)publishPlayer:(NSString *)identifier {
126
+ if (!identifier || self.sessionID || ![self.activeID isEqual:identifier]) return;
127
+ AVPlayer *player = self.players[identifier]; if (!player) return;
128
+ NSDictionary *status = [self statusForPlayer:player];
129
+ NSMutableDictionary *info = [MPNowPlayingInfoCenter.defaultCenter.nowPlayingInfo mutableCopy] ?: [NSMutableDictionary new];
130
+ info[MPMediaItemPropertyPlaybackDuration] = status[@"duration"]; info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = status[@"currentTime"]; info[MPNowPlayingInfoPropertyPlaybackRate] = @(player.rate);
131
+ MPNowPlayingInfoCenter.defaultCenter.nowPlayingInfo = info;
132
+ MPNowPlayingInfoCenter.defaultCenter.playbackState = [status[@"playing"] boolValue] ? MPNowPlayingPlaybackStatePlaying : MPNowPlayingPlaybackStatePaused;
133
+ }
58
134
  - (void)publishMetadata:(NSDictionary *)metadata {
59
135
  NSMutableDictionary *info = [MediaInfo(metadata) mutableCopy];
60
136
  MPNowPlayingInfoCenter.defaultCenter.nowPlayingInfo = info;
@@ -92,16 +168,16 @@ RCT_EXPORT_MODULE(NativeSparkAudio)
92
168
  AVPlayer *player = self.players[identifier]; id result = NSNull.null;
93
169
  if ([method hasPrefix:@"session"]) {
94
170
  if ([method isEqual:@"sessionCreate"]) {
95
- self.sessionID = identifier; self.session = [@{@"commands": @[@"play", @"pause"]} mutableCopy]; [self.commands removeAllObjects];
171
+ self.sessionID = identifier; self.session = [@{@"commands": @[@"play", @"pause"]} mutableCopy];
96
172
  [self publishMetadata:args[@"metadata"] ?: @{}];
97
- } else if (![self.sessionID isEqual:identifier]) { reject(@"E_SESSION_REPLACED", @"Media session has been replaced", nil); return; }
173
+ } else if (![self.sessionID isEqual:identifier] && [method isEqual:@"sessionRemove"]) { resolve(@"null"); return; }
174
+ else if (![self.sessionID isEqual:identifier]) { reject(@"E_CLOSED", @"Media session has been replaced", nil); return; }
98
175
  if ([method isEqual:@"sessionCreate"] || [method isEqual:@"sessionUpdate"]) {
99
176
  [self.session addEntriesFromDictionary:args];
100
177
  if (args[@"metadata"]) [self publishMetadata:args[@"metadata"]];
101
178
  [self publishSession];
102
- } else if ([method isEqual:@"sessionCommands"]) { result = [self.commands copy]; [self.commands removeAllObjects]; }
103
- else if ([method isEqual:@"sessionRemove"]) {
104
- self.sessionID = nil; self.session = nil; self.activeID = nil; ++self.artworkGeneration; [self.commands removeAllObjects];
179
+ } else if ([method isEqual:@"sessionRemove"]) {
180
+ self.sessionID = nil; self.session = nil; self.activeID = nil; ++self.artworkGeneration;
105
181
  MPNowPlayingInfoCenter.defaultCenter.nowPlayingInfo = nil; MPNowPlayingInfoCenter.defaultCenter.playbackState = MPNowPlayingPlaybackStateStopped;
106
182
  for (MPRemoteCommand *command in RemoteCommands().allValues) command.enabled = NO;
107
183
  } else { reject(@"E_AUDIO", @"Unknown session operation", nil); return; }
@@ -110,12 +186,29 @@ RCT_EXPORT_MODULE(NativeSparkAudio)
110
186
  if (!url || (!url.isFileURL && ![@[@"https", @"http"] containsObject:url.scheme])) { reject(@"E_AUDIO", @"Expected a local file or HTTP audio URL", nil); return; }
111
187
  if (url.isFileURL && ![NSFileManager.defaultManager isReadableFileAtPath:url.path]) { reject(@"E_NOT_FOUND", @"Audio file is not readable", nil); return; }
112
188
  self.players[identifier] = [AVPlayer playerWithURL:url]; self.metadata[identifier] = @{@"title": args[@"title"] ?: @"Music"};
189
+ __weak RNSparkAudio *weakSelf = self;
190
+ self.endObservers[identifier] = [NSNotificationCenter.defaultCenter addObserverForName:AVPlayerItemDidPlayToEndTimeNotification object:self.players[identifier].currentItem queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification *event) { [weakSelf publishPlayer:identifier]; }];
113
191
  if (!self.sessionID) { self.activeID = identifier; [self publishMetadata:self.metadata[identifier]]; }
114
- } else if (!player) { reject(@"E_AUDIO", @"Audio player is not available", nil); return; }
115
- else if ([method isEqual:@"ready"]) {
192
+ } else if (!player && [method isEqual:@"remove"]) { resolve(@"null"); return; }
193
+ else if (!player) { reject(@"E_AUDIO", @"Audio player is not available", nil); return; }
194
+ else if ([method isEqual:@"awaitReady"]) {
116
195
  NSError *error = player.error ?: player.currentItem.error;
117
196
  if (error) { reject(@"E_AUDIO", error.localizedDescription, error); return; }
118
- result = @(player.currentItem.status == AVPlayerItemStatusReadyToPlay);
197
+ if (player.currentItem.status == AVPlayerItemStatusReadyToPlay) { [self publishPlayer:identifier]; resolve(@"true"); return; }
198
+ if (self.readiness[identifier]) { reject(@"E_BUSY", @"Audio readiness is already pending", nil); return; }
199
+ __weak RNSparkAudio *weakSelf = self;
200
+ self.readyRejects[identifier] = reject;
201
+ self.readiness[identifier] = [[SparkAudioObservation alloc] initWithPlayer:player positionUpdates:NO notify:^{
202
+ RNSparkAudio *owner = weakSelf; AVPlayer *current = owner.players[identifier];
203
+ NSError *error = current.error ?: current.currentItem.error;
204
+ if (!owner || !current || (!error && current.currentItem.status != AVPlayerItemStatusReadyToPlay)) return;
205
+ [owner.readiness[identifier] remove]; [owner.readiness removeObjectForKey:identifier]; [owner.readyRejects removeObjectForKey:identifier];
206
+ if (error) reject(@"E_AUDIO", error.localizedDescription, error);
207
+ else { [owner publishPlayer:identifier]; resolve(@"true"); }
208
+ }];
209
+ // Readiness may have changed between the first check and installing KVO.
210
+ self.readiness[identifier].notify();
211
+ return;
119
212
  }
120
213
  else if ([method isEqual:@"volume"]) { double volume = [args[@"volume"] doubleValue]; if (!isfinite(volume) || volume < 0 || volume > 1) { reject(@"E_AUDIO", @"Invalid volume", nil); return; } player.volume = volume; }
121
214
  else if ([method isEqual:@"metadata"]) { self.metadata[identifier] = args[@"metadata"]; if (!self.sessionID && [self.activeID isEqual:identifier]) [self publishMetadata:self.metadata[identifier]]; }
@@ -127,28 +220,37 @@ RCT_EXPORT_MODULE(NativeSparkAudio)
127
220
  else if ([method isEqual:@"seek"]) {
128
221
  double seconds = [args[@"seconds"] doubleValue];
129
222
  if (!isfinite(seconds) || seconds < 0) { reject(@"E_AUDIO", @"Invalid seek position", nil); return; }
130
- [player seekToTime:CMTimeMakeWithSeconds(seconds, 600) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero completionHandler:^(BOOL finished) { if (finished) resolve(@"null"); else reject(@"E_AUDIO", @"Seek was interrupted", nil); }]; return;
223
+ [player seekToTime:CMTimeMakeWithSeconds(seconds, 600) toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero completionHandler:^(BOOL finished) { dispatch_async(dispatch_get_main_queue(), ^{ if (finished) { [self publishPlayer:identifier]; [self emitStatus:identifier]; resolve(@"null"); } else reject(@"E_AUDIO", @"Seek was interrupted", nil); }); }]; return;
131
224
  } else if ([method isEqual:@"status"]) {
132
- double position = CMTimeGetSeconds(player.currentTime), duration = CMTimeGetSeconds(player.currentItem.duration);
133
- if (!isfinite(position)) position = 0; if (!isfinite(duration)) duration = 0;
134
- NSError *error = player.error ?: player.currentItem.error;
135
- BOOL playing = player.timeControlStatus == AVPlayerTimeControlStatusPlaying;
136
- result = @{ @"volume": @(player.volume), @"playing": @(playing), @"currentTime": @(position), @"duration": @(duration), @"didJustFinish": @(duration > 0 && position >= duration && player.rate == 0), @"error": error.localizedDescription ?: NSNull.null };
137
- if (!self.sessionID && [self.activeID isEqual:identifier]) {
138
- NSMutableDictionary *info = [MPNowPlayingInfoCenter.defaultCenter.nowPlayingInfo mutableCopy] ?: [NSMutableDictionary new];
139
- info[MPMediaItemPropertyPlaybackDuration] = @(duration); info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = @(position); info[MPNowPlayingInfoPropertyPlaybackRate] = @(player.rate); MPNowPlayingInfoCenter.defaultCenter.nowPlayingInfo = info;
140
- MPNowPlayingInfoCenter.defaultCenter.playbackState = playing ? MPNowPlayingPlaybackStatePlaying : MPNowPlayingPlaybackStatePaused;
141
- }
142
- } else if ([method isEqual:@"remove"]) { [player pause]; [self.players removeObjectForKey:identifier]; [self.metadata removeObjectForKey:identifier]; if (!self.sessionID && [self.activeID isEqual:identifier]) { self.activeID = nil; ++self.artworkGeneration; MPNowPlayingInfoCenter.defaultCenter.nowPlayingInfo = nil; } }
225
+ result = [self statusForPlayer:player];
226
+ } else if ([method isEqual:@"statusUpdates"]) {
227
+ if ([args[@"enabled"] boolValue] && !self.observations[identifier]) {
228
+ __weak RNSparkAudio *weakSelf = self;
229
+ self.observations[identifier] = [[SparkAudioObservation alloc] initWithPlayer:player notify:^{ [weakSelf emitStatus:identifier]; }];
230
+ [self emitStatus:identifier];
231
+ } else if (![args[@"enabled"] boolValue]) { [self.observations[identifier] remove]; [self.observations removeObjectForKey:identifier]; [self.lastStatuses removeObjectForKey:identifier]; }
232
+ } else if ([method isEqual:@"remove"]) {
233
+ RCTPromiseRejectBlock cancelReady = self.readyRejects[identifier];
234
+ [self.readiness[identifier] remove]; [self.readiness removeObjectForKey:identifier]; [self.readyRejects removeObjectForKey:identifier];
235
+ if (cancelReady) cancelReady(@"E_CLOSED", @"Audio player was removed during loading", nil);
236
+ [self.observations[identifier] remove]; [self.observations removeObjectForKey:identifier]; [self.lastStatuses removeObjectForKey:identifier];
237
+ if (self.endObservers[identifier]) [NSNotificationCenter.defaultCenter removeObserver:self.endObservers[identifier]]; [self.endObservers removeObjectForKey:identifier];
238
+ [player pause]; [self.players removeObjectForKey:identifier]; [self.metadata removeObjectForKey:identifier]; if (!self.sessionID && [self.activeID isEqual:identifier]) { self.activeID = nil; ++self.artworkGeneration; MPNowPlayingInfoCenter.defaultCenter.nowPlayingInfo = nil; }
239
+ }
143
240
  else { reject(@"E_AUDIO", @"Unknown audio operation", nil); return; }
241
+ if ([method isEqual:@"play"] || [method isEqual:@"pause"]) [self publishPlayer:identifier];
144
242
  NSData *data = [NSJSONSerialization dataWithJSONObject:result options:NSJSONWritingFragmentsAllowed error:nil]; resolve([[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
145
243
  });
146
244
  }
147
245
  - (void)invalidate { dispatch_async(dispatch_get_main_queue(), ^{
246
+ for (SparkAudioObservation *observation in self.readiness.allValues) [observation remove]; [self.readiness removeAllObjects];
247
+ for (RCTPromiseRejectBlock reject in self.readyRejects.allValues) reject(@"E_CLOSED", @"Audio module was invalidated during loading", nil); [self.readyRejects removeAllObjects];
248
+ for (SparkAudioObservation *observation in self.observations.allValues) [observation remove]; [self.observations removeAllObjects]; [self.lastStatuses removeAllObjects];
249
+ for (id token in self.endObservers.allValues) [NSNotificationCenter.defaultCenter removeObserver:token]; [self.endObservers removeAllObjects];
148
250
  for (AVPlayer *player in self.players.allValues) [player pause];
149
251
  [self.players removeAllObjects]; [self.metadata removeAllObjects]; ++self.artworkGeneration;
150
252
  for (NSString *key in self.targets) [RemoteCommands()[key] removeTarget:self.targets[key]];
151
253
  [self.targets removeAllObjects]; self.sessionID = nil; MPNowPlayingInfoCenter.defaultCenter.nowPlayingInfo = nil;
152
- }); }
254
+ }); [super invalidate]; }
153
255
  - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:(const facebook::react::ObjCTurboModule::InitParams &)params { return std::make_shared<facebook::react::NativeSparkAudioSpecJSI>(params); }
154
256
  @end
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@legendapp/spark-audio",
3
- "version": "0.0.1-next.2",
3
+ "version": "0.0.1-next.4",
4
4
  "license": "MIT",
5
5
  "main": "src/index",
6
6
  "react-native": "src/index",
@@ -45,5 +45,8 @@
45
45
  ],
46
46
  "requires": []
47
47
  },
48
- "private": true
48
+ "private": true,
49
+ "dependencies": {
50
+ "@legendapp/spark-desktop-app": "0.0.1-next.4"
51
+ }
49
52
  }
@@ -1,3 +1,7 @@
1
1
  import { TurboModuleRegistry, type TurboModule } from "react-native";
2
- export interface Spec extends TurboModule { call(method: string, args: string): Promise<string> }
2
+ export interface Spec extends TurboModule {
3
+ call(method: string, args: string): Promise<string>;
4
+ addListener(eventName: string): void;
5
+ removeListeners(count: number): void;
6
+ }
3
7
  export default TurboModuleRegistry.get<Spec>("NativeSparkAudio");
@@ -1,39 +1,43 @@
1
- import { statusListeners } from "./status-listeners";
2
- import { validateVolume, validateMetadata } from "./media-types";
1
+ import { SparkError, invokeNative, parseNativeResult } from "@legendapp/spark-desktop-app/src/contracts";
2
+ import { playerHandle, validatePlayerOptions, waitForAudio } from "./player";
3
3
  export { createMediaSession } from "./media-session";
4
4
  export type * from "./media-types";
5
5
  import Native from "./NativeSparkAudio";
6
- import { validateSource, validateTime, type AudioPlayer, type AudioSource, type AudioStatus } from "./types";
7
- export type { AudioPlayer, AudioSource, AudioStatus } from "./types";
6
+ import { onAudioEvent } from "./events";
7
+ import { validateSource, type AudioPlayer, type AudioSource, type AudioPlayerOptions, type AudioStatus } from "./types";
8
+ export type { AudioPlayer, AudioSource, AudioStatus, AudioPlayerOptions, Subscription } from "./types";
8
9
  let sequence = 0;
9
- export async function createAudioPlayer(source: AudioSource): Promise<AudioPlayer> {
10
- validateSource(source);
11
- if (!Native) throw Object.assign(new Error("Audio playback is unavailable. Build a client with @legendapp/spark-audio."), { code: "E_UNAVAILABLE" });
10
+ export async function createAudioPlayer(source: AudioSource, options: AudioPlayerOptions = {}): Promise<AudioPlayer> {
11
+ validateSource(source); validatePlayerOptions(options);
12
+ options = { ...options };
13
+ source = { ...source };
14
+ if (!Native) throw new SparkError("E_MODULE_UNAVAILABLE", "Audio requires a client with @legendapp/spark-audio");
12
15
  const native = Native, id = `audio-${Date.now()}-${++sequence}`;
13
- let removed = false;
14
- const call = async <T = void>(method: string, args = {}): Promise<T> => {
15
- if (removed) throw new Error("Audio player has been removed");
16
- return JSON.parse(await native.call(method, JSON.stringify({ id, ...args })));
17
- };
18
- await call("create", source);
19
- let timer: ReturnType<typeof setTimeout> | undefined;
20
- let cancelled = false;
16
+ const call = async (method: string, args = {}): Promise<unknown> => parseNativeResult(await invokeNative(() => native.call(method, JSON.stringify({ id, ...args }))), (value): value is unknown => true);
17
+ const command = async (method: string, args = {}) => { if (await call(method, args) !== null) throw new SparkError("E_INVALID_DATA", "Invalid audio command result"); };
21
18
  try {
22
- await Promise.race([
23
- (async () => { while (!cancelled && !await call<boolean>("ready")) await new Promise(resolve => setTimeout(resolve, 50)); })(),
24
- new Promise<never>((_, reject) => { timer = setTimeout(() => reject(new Error("Audio loading timed out")), 15_000); }),
25
- ]);
26
- } catch (error) { void call("remove").catch(() => {}); throw error; }
27
- finally { cancelled = true; clearTimeout(timer); }
28
- const read = () => call<AudioStatus>("status");
29
- const listeners = statusListeners(read);
30
- return {
31
- setVolume: volume => { validateVolume(volume); return call("volume", { volume }); },
32
- setMetadata: metadata => { validateMetadata(metadata); return call("metadata", { metadata }); },
33
- addListener: (event, listener) => { if (event !== "playbackStatusUpdate") throw new TypeError("Unknown audio event"); return listeners.add(listener); },
34
- play: () => call("play"), pause: () => call("pause"),
35
- seekTo: seconds => { validateTime(seconds); return call("seek", { seconds }); },
36
- getStatus: read,
37
- async remove() { if (!removed) { await call("remove"); removed = true; listeners.close(); } },
38
- };
19
+ await command("create", source);
20
+ // The native promise settles on AVPlayer/MediaPlayer readiness; no JS polling.
21
+ await waitForAudio(async () => { const ready = await call("awaitReady"); if (ready !== true) throw new SparkError("E_INVALID_DATA", "Invalid audio readiness"); return true; }, options);
22
+ } catch (cause) {
23
+ try { await command("remove"); } catch (cleanup) { throw new SparkError("E_NATIVE", "Audio loading and cleanup failed", { cause: new AggregateError([cause, cleanup]) }); }
24
+ throw cause;
25
+ }
26
+ return playerHandle({
27
+ setVolume: volume => command("volume", { volume }),
28
+ setMetadata: metadata => command("metadata", { metadata: metadata ?? {} }),
29
+ play: () => command("play"), pause: () => command("pause"),
30
+ seekTo: seconds => command("seek", { seconds }),
31
+ getStatus: async () => await call("status") as AudioStatus,
32
+ addStatusListener(listener) {
33
+ let active = true;
34
+ const subscription = onAudioEvent("sparkAudioStatus", id, value => { if (active) listener(value as AudioStatus); });
35
+ void command("statusUpdates", { enabled: true }).catch(error => { if (active) console.error(error); });
36
+ return { remove() { active = false; subscription.remove(); void command("statusUpdates", { enabled: false }).catch(console.error); } };
37
+ },
38
+ remove: () => command("remove"),
39
+ });
39
40
  }
41
+
42
+ export { useAudioPlayer } from "./hooks";
43
+ export type { AudioPlayerState, AudioPlayerHookOptions } from "./hooks";
@@ -0,0 +1,11 @@
1
+ import { NativeEventEmitter } from "react-native";
2
+ import { SparkError, type Subscription } from "@legendapp/spark-desktop-app/src/contracts";
3
+ import Native from "./NativeSparkAudio";
4
+
5
+ /** Private audio transport. Install before native setup so readiness cannot lose events. */
6
+ export function onAudioEvent(name: "sparkAudioStatus" | "sparkMediaCommand", id: string, listener: (value: unknown) => void): Subscription {
7
+ if (!Native) throw new SparkError("E_MODULE_UNAVAILABLE", "The audio module is unavailable");
8
+ return new NativeEventEmitter(Native).addListener(name, (event: unknown) => {
9
+ if (event && typeof event === "object" && (event as { id?: unknown }).id === id) listener((event as { value?: unknown }).value);
10
+ });
11
+ }
@@ -0,0 +1,65 @@
1
+ import { useEffect, useLayoutEffect, useRef, useState } from "react";
2
+ import { createAudioPlayer } from "./index";
3
+ import type { AudioPlayer, AudioPlayerOptions, AudioSource } from "./types";
4
+
5
+ export type AudioPlayerState = { status: "loading" } | { status: "ready"; player: AudioPlayer } | { status: "error"; error: unknown };
6
+ export interface AudioPlayerHookOptions extends AudioPlayerOptions {
7
+ /** Cleanup failures cannot update an unmounted component. The player permits retry. */
8
+ onCleanupError?(error: unknown, player: AudioPlayer): void;
9
+ }
10
+ type OwnedPlayer = { player: AudioPlayer; removing?: Promise<void> };
11
+ type Lifecycle = { tail: Promise<void>; owner?: OwnedPlayer };
12
+ /** Owns one player for this source. Creation occurs in an effect, never during render. */
13
+ export function useAudioPlayer(source: AudioSource, options: AudioPlayerHookOptions = {}): AudioPlayerState {
14
+ const { uri, title } = source;
15
+ const { loadTimeoutMs, signal, onCleanupError } = options;
16
+ const cleanupError = useRef(onCleanupError);
17
+ const lifecycleRef = useRef<Lifecycle | null>(null);
18
+ if (!lifecycleRef.current) lifecycleRef.current = { tail: Promise.resolve() };
19
+ const lifecycle = lifecycleRef.current;
20
+ useLayoutEffect(() => { cleanupError.current = onCleanupError; }, [onCleanupError]);
21
+ const [current, setCurrent] = useState<{ uri: string; title?: string; loadTimeoutMs?: number; signal?: AbortSignal; state: AudioPlayerState }>();
22
+ useEffect(() => {
23
+ const controller = new AbortController();
24
+ let disposed = false, owned: OwnedPlayer | undefined;
25
+ const abort = () => controller.abort();
26
+ signal?.addEventListener("abort", abort, { once: true });
27
+ if (signal?.aborted) abort();
28
+ const set = (state: AudioPlayerState) => { if (!disposed) setCurrent({ uri, title, loadTimeoutMs, signal, state }); };
29
+ set({ status: "loading" });
30
+ const remove = (owner: OwnedPlayer) => {
31
+ if (!owner.removing) owner.removing = owner.player.remove().then(() => {
32
+ if (lifecycle.owner === owner) lifecycle.owner = undefined;
33
+ }).catch(error => {
34
+ try {
35
+ if (cleanupError.current) cleanupError.current(error, owner.player);
36
+ else console.error(error, owner.player);
37
+ } catch (callbackError) { console.error(callbackError); }
38
+ throw error;
39
+ }).finally(() => { owner.removing = undefined; });
40
+ return owner.removing;
41
+ };
42
+ const creation = lifecycle.tail.then(async () => {
43
+ if (disposed) return;
44
+ try {
45
+ if (lifecycle.owner) await remove(lifecycle.owner);
46
+ } catch (error) { set({ status: "error", error }); return; }
47
+ if (disposed) return;
48
+ try {
49
+ const player = await createAudioPlayer({ uri, title }, { loadTimeoutMs, signal: controller.signal });
50
+ owned = { player };
51
+ lifecycle.owner = owned;
52
+ if (!disposed) set({ status: "ready", player });
53
+ } catch (error) { set({ status: "error", error }); }
54
+ });
55
+ lifecycle.tail = creation;
56
+ return () => {
57
+ disposed = true;
58
+ controller.abort();
59
+ signal?.removeEventListener("abort", abort);
60
+ const cleanup = creation.then(async () => { if (owned) await remove(owned).catch(() => {}); });
61
+ lifecycle.tail = cleanup;
62
+ };
63
+ }, [uri, title, loadTimeoutMs, signal]);
64
+ return current && current.uri === uri && current.title === title && current.loadTimeoutMs === loadTimeoutMs && current.signal === signal ? current.state : { status: "loading" };
65
+ }
@@ -1,42 +1,101 @@
1
- import { validateVolume, validateMetadata } from "./media-types";
2
- import { statusListeners } from "./status-listeners";
1
+ import { SparkError } from "@legendapp/spark-desktop-app/src/contracts";
2
+ import { playerHandle, validatePlayerOptions, waitForAudio } from "./player";
3
3
  import { hasExplicitMediaSession, claimPlayerMediaSession, ownsPlayerMediaSession, releasePlayerMediaSession } from "./media-session.web";
4
4
  export { createMediaSession } from "./media-session.web";
5
5
  export type * from "./media-types";
6
- import { validateSource, validateTime, type AudioPlayer, type AudioSource } from "./types";
7
- export type { AudioPlayer, AudioSource, AudioStatus } from "./types";
6
+ import { validateSource, type AudioPlayer, type AudioSource, type AudioPlayerOptions, type AudioStatus } from "./types";
7
+ export type { AudioPlayer, AudioSource, AudioStatus, AudioPlayerOptions, Subscription } from "./types";
8
+ export { useAudioPlayer } from "./hooks";
9
+ export type { AudioPlayerState, AudioPlayerHookOptions } from "./hooks";
8
10
 
9
- export async function createAudioPlayer(source: AudioSource): Promise<AudioPlayer> {
10
- validateSource(source);
11
+ const SEEK_TIMEOUT_MS = 10_000;
12
+
13
+ export async function createAudioPlayer(source: AudioSource, options: AudioPlayerOptions = {}): Promise<AudioPlayer> {
14
+ validateSource(source); validatePlayerOptions(options);
15
+ options = { ...options };
16
+ source = { ...source };
17
+ if (typeof Audio === "undefined") throw new SparkError("E_UNSUPPORTED_PLATFORM", "Browser audio is unavailable");
11
18
  const audio = new Audio(source.uri);
12
- audio.preload = "metadata";
13
- let removed = false;
14
- const pending = new Set<() => void>();
15
- const alive = () => { if (removed) throw new Error("Audio player has been removed"); };
16
- const media = navigator.mediaSession;
17
- if (media && !hasExplicitMediaSession()) {
18
- claimPlayerMediaSession(audio);
19
- media.metadata = new MediaMetadata({ title: source.title ?? "Music" });
20
- media.setActionHandler("play", () => { void audio.play().catch(console.error); });
21
- media.setActionHandler("pause", () => audio.pause());
22
- media.setActionHandler("seekto", event => { if (event.seekTime !== undefined) audio.currentTime = event.seekTime; });
19
+ audio.preload = "auto";
20
+ const media = typeof navigator === "undefined" ? undefined : navigator.mediaSession;
21
+ let cancelPendingSeek: ((error: SparkError) => void) | undefined;
22
+ let removeRequested = false;
23
+ const statusValue = (): AudioStatus => ({ playing: !audio.paused, currentTime: audio.currentTime, duration: Number.isFinite(audio.duration) ? audio.duration : 0, didJustFinish: audio.ended, error: audio.error?.message ?? null, volume: audio.volume });
24
+ async function dispose() {
25
+ audio.pause(); audio.removeAttribute("src"); audio.load();
26
+ if (media && ownsPlayerMediaSession(audio)) {
27
+ for (const action of ["play", "pause", "seekto"] as const) media.setActionHandler(action, null);
28
+ media.metadata = null; releasePlayerMediaSession(audio);
29
+ }
30
+ }
31
+ try {
32
+ await waitForAudio(async () => { if (audio.error) throw new SparkError("E_NATIVE", audio.error.message || "Could not load audio"); return audio.readyState >= 1; }, options, notify => {
33
+ audio.addEventListener("loadedmetadata", notify); audio.addEventListener("error", notify);
34
+ return { remove() { audio.removeEventListener("loadedmetadata", notify); audio.removeEventListener("error", notify); } };
35
+ });
36
+ if (media && !hasExplicitMediaSession()) {
37
+ claimPlayerMediaSession(audio);
38
+ media.metadata = new MediaMetadata({ title: source.title ?? "Music" });
39
+ media.setActionHandler("play", () => { void audio.play().catch(console.error); });
40
+ media.setActionHandler("pause", () => audio.pause());
41
+ media.setActionHandler("seekto", event => { if (event.seekTime !== undefined) audio.currentTime = event.seekTime; });
42
+ }
43
+ } catch (cause) {
44
+ try { await dispose(); } catch (cleanup) { throw new SparkError("E_NATIVE", "Audio loading and cleanup failed", { cause: new AggregateError([cause, cleanup]) }); }
45
+ throw cause;
23
46
  }
24
- const read = async () => { alive(); return { playing: !audio.paused, currentTime: audio.currentTime, duration: Number.isFinite(audio.duration) ? audio.duration : 0, didJustFinish: audio.ended, error: audio.error?.message ?? null, volume: audio.volume }; };
25
- const listeners = statusListeners(read);
47
+ const player = playerHandle({
48
+ async setVolume(volume) { audio.volume = volume; },
49
+ async setMetadata(metadata) {
50
+ const value = metadata ?? {};
51
+ if (media && ownsPlayerMediaSession(audio)) media.metadata = new MediaMetadata({ title: value.title, artist: value.artist, album: value.albumTitle, artwork: value.artworkUrl ? [{ src: value.artworkUrl }] : [] });
52
+ },
53
+ async play() { await audio.play(); }, async pause() { audio.pause(); },
54
+ async seekTo(seconds) {
55
+ if (removeRequested) throw new SparkError("E_CLOSED", "Audio player was removed during seek");
56
+ if (audio.error) throw new SparkError("E_NATIVE", audio.error.message || "Could not seek audio", { cause: audio.error });
57
+ if (!audio.seeking && audio.currentTime === seconds) return;
58
+ await new Promise<void>((resolve, reject) => {
59
+ let settled = false;
60
+ const finish = (error?: SparkError) => {
61
+ if (settled) return;
62
+ settled = true;
63
+ clearTimeout(timeout);
64
+ audio.removeEventListener("seeked", onSeeked);
65
+ audio.removeEventListener("error", onError);
66
+ audio.removeEventListener("emptied", onEmptied);
67
+ if (cancelPendingSeek === cancel) cancelPendingSeek = undefined;
68
+ if (error) reject(error); else resolve();
69
+ };
70
+ const onSeeked = () => { if (!audio.seeking) finish(); };
71
+ const onError = () => finish(new SparkError("E_NATIVE", audio.error?.message || "Could not seek audio", { cause: audio.error }));
72
+ const onEmptied = () => finish(new SparkError("E_CLOSED", "Audio source was removed during seek"));
73
+ const cancel = (error: SparkError) => finish(error);
74
+ const timeout = setTimeout(() => finish(new SparkError("E_TIMEOUT", "Browser audio seek did not complete before timeout")), SEEK_TIMEOUT_MS);
75
+ audio.addEventListener("seeked", onSeeked);
76
+ audio.addEventListener("error", onError);
77
+ audio.addEventListener("emptied", onEmptied);
78
+ cancelPendingSeek = cancel;
79
+ try { audio.currentTime = seconds; }
80
+ catch (cause) { finish(new SparkError("E_NATIVE", "Failed to seek browser audio", { cause })); }
81
+ });
82
+ },
83
+ async getStatus() { return statusValue(); },
84
+ addStatusListener(listener) {
85
+ const events = ["playing", "pause", "timeupdate", "ended", "durationchange", "volumechange", "error", "seeked"];
86
+ const notify = () => listener(statusValue());
87
+ for (const event of events) audio.addEventListener(event, notify);
88
+ notify();
89
+ return { remove() { for (const event of events) audio.removeEventListener(event, notify); } };
90
+ },
91
+ remove: dispose,
92
+ });
26
93
  return {
27
- async setVolume(volume) { alive(); validateVolume(volume); audio.volume = volume; },
28
- async setMetadata(metadata) { alive(); validateMetadata(metadata); if (media && ownsPlayerMediaSession(audio)) media.metadata = new MediaMetadata({ title: metadata.title, artist: metadata.artist, album: metadata.albumTitle, artwork: metadata.artworkUrl ? [{ src: metadata.artworkUrl }] : [] }); },
29
- addListener(event, listener) { if (event !== "playbackStatusUpdate") throw new TypeError("Unknown audio event"); return listeners.add(listener); },
30
- async play() { alive(); await audio.play(); }, async pause() { alive(); audio.pause(); },
31
- async seekTo(seconds) { alive(); validateTime(seconds); if (audio.readyState === 0) await new Promise<void>((resolve, reject) => {
32
- const clear = () => { clearTimeout(timer); pending.delete(cancel); audio.removeEventListener("loadedmetadata", loaded); audio.removeEventListener("error", failed); };
33
- const loaded = () => { clear(); resolve(); }; const failed = () => { clear(); reject(new Error("Could not load audio")); };
34
- const cancel = () => { clear(); reject(new Error("Audio player has been removed")); };
35
- const timer = setTimeout(() => { clear(); reject(new Error("Audio loading timed out")); }, 15_000);
36
- pending.add(cancel);
37
- audio.addEventListener("loadedmetadata", loaded); audio.addEventListener("error", failed);
38
- }); alive(); audio.currentTime = seconds; },
39
- getStatus: read,
40
- async remove() { if (!removed) { removed = true; listeners.close(); for (const cancel of pending) cancel(); audio.pause(); audio.removeAttribute("src"); audio.load(); if (media && ownsPlayerMediaSession(audio)) { for (const action of ["play", "pause", "seekto"] as const) media.setActionHandler(action, null); media.metadata = null; releasePlayerMediaSession(audio); } } },
94
+ ...player,
95
+ remove() {
96
+ removeRequested = true;
97
+ cancelPendingSeek?.(new SparkError("E_CLOSED", "Audio player was removed during seek"));
98
+ return player.remove();
99
+ },
41
100
  };
42
101
  }
@@ -1,5 +1,7 @@
1
- import type { MediaCommand, MediaSession, MediaSessionOptions } from "./media-types";
2
- /** Expo Audio owns mobile transport controls; it cannot attach an external engine. */
3
- export async function createMediaSession(_options: MediaSessionOptions, _onCommand: (command: MediaCommand) => void): Promise<MediaSession> {
4
- throw Object.assign(new Error("Standalone media sessions support desktop and web. On mobile use the audio player's Expo-backed controls."), { code: "E_UNAVAILABLE" });
1
+ import { SparkError } from "@legendapp/spark-desktop-app/src/contracts";
2
+ import { sessionOptions, type MediaSession, type MediaSessionOptions } from "./media-types";
3
+ /** Expo Audio cannot attach an external playback engine to its transport controls. */
4
+ export async function createMediaSession(options: MediaSessionOptions): Promise<MediaSession> {
5
+ sessionOptions(options);
6
+ throw new SparkError("E_UNSUPPORTED_PLATFORM", "Standalone media sessions require desktop or web; mobile uses the player's controls");
5
7
  }