@legendapp/spark 0.0.1-next.3 → 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 (83) hide show
  1. package/camera.ts +13 -0
  2. package/package.json +21 -11
  3. package/patches/camera/react-native-nitro-image.patch +320 -0
  4. package/patches/camera/react-native-nitro-modules.patch +50 -0
  5. package/patches/camera/react-native-vision-camera.patch +2662 -0
  6. package/patches/camera/upstream.json +22 -0
  7. package/patches/dependencies.json +8 -0
  8. package/patches/react-native-enriched-markdown@1.0.2.patch +8684 -0
  9. package/postinstall.cjs +1 -0
  10. package/ui-swipe.ts +1 -0
  11. package/vendor/node_modules/@legendapp/spark-ai/package.json +3 -3
  12. package/vendor/node_modules/@legendapp/spark-audio/package.json +2 -2
  13. package/vendor/node_modules/@legendapp/spark-auth-session/package.json +3 -3
  14. package/vendor/node_modules/@legendapp/spark-cli/dist/build.js +62 -11
  15. package/vendor/node_modules/@legendapp/spark-cli/dist/cli-arguments.js +2 -1
  16. package/vendor/node_modules/@legendapp/spark-cli/dist/commands.js +3 -2
  17. package/vendor/node_modules/@legendapp/spark-cli/dist/create.js +6 -3
  18. package/vendor/node_modules/@legendapp/spark-cli/dist/credentials.js +41 -26
  19. package/vendor/node_modules/@legendapp/spark-cli/dist/dependency-pins.js +132 -0
  20. package/vendor/node_modules/@legendapp/spark-cli/dist/dev-command.js +1 -1
  21. package/vendor/node_modules/@legendapp/spark-cli/dist/dev.js +1 -1
  22. package/vendor/node_modules/@legendapp/spark-cli/dist/errors.js +100 -0
  23. package/vendor/node_modules/@legendapp/spark-cli/dist/examples.js +1 -1
  24. package/vendor/node_modules/@legendapp/spark-cli/dist/expo-native.cjs +5 -1
  25. package/vendor/node_modules/@legendapp/spark-cli/dist/helpers.js +1 -1
  26. package/vendor/node_modules/@legendapp/spark-cli/dist/index.js +43 -7
  27. package/vendor/node_modules/@legendapp/spark-cli/dist/json.js +16 -0
  28. package/vendor/node_modules/@legendapp/spark-cli/dist/native-patch-preflight.js +3 -1
  29. package/vendor/node_modules/@legendapp/spark-cli/dist/process.js +7 -0
  30. package/vendor/node_modules/@legendapp/spark-cli/dist/project.js +27 -22
  31. package/vendor/node_modules/@legendapp/spark-cli/dist/release.js +2 -1
  32. package/vendor/node_modules/@legendapp/spark-cli/dist/release.json +19 -19
  33. package/vendor/node_modules/@legendapp/spark-cli/dist/universal.cjs +2 -1
  34. package/vendor/node_modules/@legendapp/spark-cli/dist/windows.js +1 -8
  35. package/vendor/node_modules/@legendapp/spark-cli/package.json +3 -3
  36. package/vendor/node_modules/@legendapp/spark-cli/templates/blank-typescript/package.json +2 -2
  37. package/vendor/node_modules/@legendapp/spark-cli/templates/universal/package.json +2 -2
  38. package/vendor/node_modules/@legendapp/spark-cli/templates/windows/package.json +2 -2
  39. package/vendor/node_modules/@legendapp/spark-clipboard/package.json +2 -2
  40. package/vendor/node_modules/@legendapp/spark-codex/package.json +3 -3
  41. package/vendor/node_modules/@legendapp/spark-commands/package.json +5 -5
  42. package/vendor/node_modules/@legendapp/spark-context-menu/package.json +2 -2
  43. package/vendor/node_modules/@legendapp/spark-desktop-app/package.json +1 -1
  44. package/vendor/node_modules/@legendapp/spark-desktop-app/src/contracts/menu.ts +6 -4
  45. package/vendor/node_modules/@legendapp/spark-desktop-config/config.cjs +2 -0
  46. package/vendor/node_modules/@legendapp/spark-desktop-config/expo.cjs +1 -0
  47. package/vendor/node_modules/@legendapp/spark-desktop-config/native-patches.cjs +106 -0
  48. package/vendor/node_modules/@legendapp/spark-desktop-config/native-patches.d.cts +1 -0
  49. package/vendor/node_modules/@legendapp/spark-desktop-config/package.json +5 -4
  50. package/vendor/node_modules/@legendapp/spark-desktop-host/package.json +2 -2
  51. package/vendor/node_modules/@legendapp/spark-desktop-links/package.json +2 -2
  52. package/vendor/node_modules/@legendapp/spark-desktop-shortcuts/package.json +2 -2
  53. package/vendor/node_modules/@legendapp/spark-desktop-windows/macos/window-manager/RNWindowManager.mm +39 -0
  54. package/vendor/node_modules/@legendapp/spark-desktop-windows/package.json +3 -3
  55. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/api.ts +3 -1
  56. package/vendor/node_modules/@legendapp/spark-desktop-windows/src/macos-adapter.ts +1 -1
  57. package/vendor/node_modules/@legendapp/spark-documents/package.json +7 -7
  58. package/vendor/node_modules/@legendapp/spark-drag-drop/package.json +2 -2
  59. package/vendor/node_modules/@legendapp/spark-file-dialog/package.json +2 -2
  60. package/vendor/node_modules/@legendapp/spark-file-system/package.json +2 -2
  61. package/vendor/node_modules/@legendapp/spark-global-shortcuts/package.json +2 -2
  62. package/vendor/node_modules/@legendapp/spark-message-dialog/package.json +2 -2
  63. package/vendor/node_modules/@legendapp/spark-native-menu/package.json +2 -2
  64. package/vendor/node_modules/@legendapp/spark-notifications/package.json +2 -2
  65. package/vendor/node_modules/@legendapp/spark-notifications/windows/SparkNotifications/SparkNotifications.h +16 -11
  66. package/vendor/node_modules/@legendapp/spark-processes/package.json +2 -2
  67. package/vendor/node_modules/@legendapp/spark-secure-storage/package.json +2 -2
  68. package/vendor/node_modules/@legendapp/spark-settings/package.json +3 -3
  69. package/vendor/node_modules/@legendapp/spark-settings-ui/package.json +4 -4
  70. package/vendor/node_modules/@legendapp/spark-sqlite/package.json +3 -3
  71. package/vendor/node_modules/@legendapp/spark-system/package.json +2 -2
  72. package/vendor/node_modules/@legendapp/spark-tray/package.json +2 -2
  73. package/vendor/node_modules/@legendapp/spark-ui/macos/appkit-split-view/RNSidebarSplitViewComponent.mm +263 -26
  74. package/vendor/node_modules/@legendapp/spark-ui/macos/swipe-actions/RNSwipeActions.h +10 -0
  75. package/vendor/node_modules/@legendapp/spark-ui/macos/swipe-actions/RNSwipeActions.mm +422 -0
  76. package/vendor/node_modules/@legendapp/spark-ui/package.json +3 -2
  77. package/vendor/node_modules/@legendapp/spark-ui/src/appkit-split-view/SidebarSplitViewNativeComponent.ts +6 -0
  78. package/vendor/node_modules/@legendapp/spark-ui/src/appkit-split-view/index.tsx +20 -11
  79. package/vendor/node_modules/@legendapp/spark-ui/src/swipe-actions/SwipeActionsNativeComponent.ts +11 -0
  80. package/vendor/node_modules/@legendapp/spark-ui/src/swipe-actions/index.tsx +41 -0
  81. package/vendor/node_modules/@legendapp/spark-updates/package.json +2 -2
  82. package/vendor/node_modules/@legendapp/spark-webview/package.json +2 -2
  83. package/vendor/node_modules/@legendapp/spark-window-options/package.json +1 -1
package/camera.ts ADDED
@@ -0,0 +1,13 @@
1
+ export {
2
+ Camera, VisionCamera, getCameraDevice, getAllCameraDevices, getDefaultCameraDevice,
3
+ addOnCameraDevicesChangedListener, useCamera, useCameraDevice, useCameraDevices,
4
+ useCameraPermission, useMicrophonePermission, usePhotoOutput, useVideoOutput,
5
+ } from "react-native-vision-camera";
6
+ export type {
7
+ CameraDevice, CameraRef, CameraViewProps, CameraProps, CameraSession,
8
+ CameraSessionConfig, CameraSessionConfiguration, CameraController, CameraFactory,
9
+ CameraPhotoOutput, CameraVideoOutput, PhotoOutputOptions, VideoOutputOptions,
10
+ Photo, Recorder, PermissionStatus, PermissionState, Constraint,
11
+ } from "react-native-vision-camera";
12
+ export { NitroImage } from "react-native-nitro-image";
13
+ export type { Image } from "react-native-nitro-image";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@legendapp/spark",
3
- "version": "0.0.1-next.3",
3
+ "version": "0.0.1-next.4",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -61,6 +61,7 @@
61
61
  "./shortcuts/commands": "./shortcuts-commands.ts",
62
62
  "./shortcuts/keyboard": "./shortcuts-keyboard.ts",
63
63
  "./ui/glass": "./ui-glass.ts",
64
+ "./ui/swipe": "./ui-swipe.ts",
64
65
  "./ui/symbol": "./ui-symbol.ts",
65
66
  "./ui/search": "./ui-search.ts",
66
67
  "./settings/window": "./settings-window.ts",
@@ -70,14 +71,16 @@
70
71
  "./windows/macos": "./windows-macos.ts",
71
72
  "./diagnostics": "./diagnostics.ts",
72
73
  "./ai": "./ai.ts",
73
- "./ai/codex": "./ai-codex.ts"
74
+ "./ai/codex": "./ai-codex.ts",
75
+ "./camera": "./camera.ts"
74
76
  },
75
77
  "files": [
76
78
  "*.ts",
77
79
  "*.cjs",
78
80
  "schema.json",
79
81
  "bin",
80
- "*.d.cts"
82
+ "*.d.cts",
83
+ "patches"
81
84
  ],
82
85
  "dependencies": {
83
86
  "base64-js": "1.5.1",
@@ -86,13 +89,17 @@
86
89
  "npm": "11.21.0",
87
90
  "typescript": "5.9.3",
88
91
  "cross-spawn": "7.0.6",
89
- "react-native-nitro-modules": "0.35.7",
92
+ "react-native-nitro-modules": "0.37.0",
90
93
  "@legendapp/state": "^3.0.0-beta.42",
91
94
  "expo-desktop-config-plugins": "1.2.0",
92
95
  "@expo/config-plugins": "54.0.5",
93
96
  "@expo/config-types": "54.0.10",
97
+ "diff": "8.0.2",
94
98
  "@react-native-runtimes/core": "0.1.0-alpha.2",
95
99
  "@react-native-community/cli-types": "20.1.3",
100
+ "react-native-vision-camera": "5.2.3",
101
+ "react-native-nitro-image": "0.15.2",
102
+ "react-native-enriched-markdown": "1.0.2",
96
103
  "@op-engineering/op-sqlite": "18.2.1",
97
104
  "clsx": "^2.1.1",
98
105
  "tailwind-merge": "^3.4.0",
@@ -157,29 +164,32 @@
157
164
  "access": "public",
158
165
  "tag": "next"
159
166
  },
167
+ "scripts": {
168
+ "postinstall": "node postinstall.cjs"
169
+ },
160
170
  "spark": {
161
171
  "nativePatchRequirements": {
162
172
  "schema": 1,
163
- "frameworkVersion": "0.0.1-next.3",
173
+ "frameworkVersion": "0.0.1-next.4",
164
174
  "packages": {
165
175
  "@react-native-runtimes/core": {
166
176
  "version": "0.1.0-alpha.2",
167
- "patchHash": "a216ebc90023b134b0120a89cc5417ade005b4cd60e3159bec695f2353c7d947",
177
+ "patchHash": "985164a87eb04010a8d849f5b0ca22a76548dc3f73a64a6183683ca90932ec2c",
168
178
  "upstreamRevision": "58710c25c6e505dcc1292ee54d855408a6f7a42d"
169
179
  },
170
180
  "react-native-nitro-modules": {
171
- "version": "0.35.7",
172
- "patchHash": "1027bbf2aef8530ee4abf28d6d625888f0b1497e68813fc6e044b5ae992a72eb",
173
- "upstreamIntegrity": "sha512-3EiU27EmnxTlD3pAXR2OBWeDg7+9tdjKrNQOPX08rFX5hJPNIT/h1i2PjvTUieOypCGTEi9nW/SNBtK3F+4HYg=="
181
+ "version": "0.37.0",
182
+ "patchHash": "bd582f6bd686bd9058a2eae193854e86a1f66d0d8334fbcafd2b0cab16dadaa4",
183
+ "upstreamIntegrity": "sha512-ULS2CBZcdwGw5d4Y35EV/tbCOsNsQOnQ9LC9CehMhDNm5c90lwF/LxRamzKuFpjI547G7teTQgD0peqYSGnP9g=="
174
184
  },
175
185
  "@op-engineering/op-sqlite": {
176
186
  "version": "18.2.1",
177
- "patchHash": "4168be739bed62533e0c3e12e38d34b6eed4e01305b5c6f5c3557d866e11d0de",
187
+ "patchHash": "d535dcd20aa0f5bcf8860086eb6484c73b1e84d8a586ea9fe74dca76085955a0",
178
188
  "upstreamIntegrity": "sha512-atVOtXyFzO0e454eVS27AMwRs515URCKrkvLLG+mGKcyiF5UPfC53oukpNp2UmVzlLebyTEXNkTYs6n0dVGd0g=="
179
189
  },
180
190
  "react-native-webview": {
181
191
  "version": "16.0.0",
182
- "patchHash": "892e4beb756db6f57fbe5f64f75bb4f66cc801a21bbeb82223983c7cf079f896",
192
+ "patchHash": "43cfd2d36780e085223bc7497719c7c64ec1bea1da82362e751f9937a0349e1a",
183
193
  "upstreamIntegrity": "sha512-KIg5Yhy04lQfHdXC1JQPM+0kftMfPnH6AqDH7v6YskQ9kzfUacooofOFRmbaZHASq/0WY87Fx0peiFETFjOcCg=="
184
194
  },
185
195
  "expo-audio": {
@@ -0,0 +1,320 @@
1
+ diff --git a/NitroImage.podspec b/NitroImage.podspec
2
+ index 4b8f577..6aa9982 100644
3
+ --- a/NitroImage.podspec
4
+ +++ b/NitroImage.podspec
5
+ @@ -10,14 +10,18 @@ Pod::Spec.new do |s|
6
+ s.license = package["license"]
7
+ s.authors = package["author"]
8
+
9
+ - s.platforms = { :ios => min_ios_version_supported, :visionos => 1.0 }
10
+ + s.platforms = { :ios => min_ios_version_supported, :visionos => 1.0, :osx => "14.0" }
11
+ + s.osx.prefix_header_contents = "#include <CoreGraphics/CoreGraphics.h>"
12
+ s.source = { :git => "https://github.com/mrousavy/nitro.git", :tag => "#{s.version}" }
13
+
14
+ + s.ios.source_files = "ios/**/*.{swift,m,mm}"
15
+ + s.visionos.source_files = "ios/**/*.{swift,m,mm}"
16
+ + s.osx.source_files = "macos/**/*.swift"
17
+ s.source_files = [
18
+ # Implementation (Swift)
19
+ - "ios/**/*.{swift}",
20
+ +
21
+ # Autolinking/Registration (Objective-C++)
22
+ - "ios/**/*.{m,mm}",
23
+ +
24
+ # Implementation (C++ objects)
25
+ "cpp/**/*.{hpp,cpp}",
26
+ ]
27
+ diff --git a/macos/MacImage.swift b/macos/MacImage.swift
28
+ new file mode 100644
29
+ index 0000000..3c5623e
30
+ --- /dev/null
31
+ +++ b/macos/MacImage.swift
32
+ @@ -0,0 +1,183 @@
33
+ +import AppKit
34
+ +import CoreGraphics
35
+ +import CoreImage
36
+ +import ImageIO
37
+ +import UniformTypeIdentifiers
38
+ +import NitroModules
39
+ +
40
+ +public protocol NativeImage { var cgImage: CGImage { get } }
41
+ +
42
+ +private func bitmap(_ width: Double, _ height: Double) throws -> CGContext {
43
+ + guard width.isFinite, height.isFinite, width > 0, height > 0, width <= 32768, height <= 32768,
44
+ + let context = CGContext(data: nil, width: Int(width.rounded(.up)), height: Int(height.rounded(.up)), bitsPerComponent: 8, bytesPerRow: Int(width.rounded(.up)) * 4, space: CGColorSpace(name: CGColorSpace.sRGB)!, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) else {
45
+ + throw RuntimeError.error(withMessage: "Invalid image dimensions")
46
+ + }
47
+ + return context
48
+ +}
49
+ +
50
+ +public final class HybridCGImage: HybridImageSpec, NativeImage {
51
+ + public let cgImage: CGImage
52
+ + public init(cgImage: CGImage) { self.cgImage = cgImage; super.init() }
53
+ + public var width: Double { Double(cgImage.width) }
54
+ + public var height: Double { Double(cgImage.height) }
55
+ + public var memorySize: Int { cgImage.bytesPerRow * cgImage.height }
56
+ + private func render(_ width: Double, _ height: Double, draw: (CGContext) -> Void) throws -> any HybridImageSpec {
57
+ + let context = try bitmap(width, height); draw(context)
58
+ + guard let image = context.makeImage() else { throw RuntimeError.error(withMessage: "Failed to render image") }
59
+ + return HybridCGImage(cgImage: image)
60
+ + }
61
+ + public func toRawPixelData(allowGpu: Bool?) throws -> RawPixelData {
62
+ + let context = try bitmap(width, height)
63
+ + context.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
64
+ + guard let pointer = context.data else { throw RuntimeError.error(withMessage: "Missing pixel data") }
65
+ + let data = Data(bytes: pointer, count: context.bytesPerRow * context.height)
66
+ + return RawPixelData(buffer: try ArrayBuffer.copy(data: data), width: width, height: height, pixelFormat: .rgba)
67
+ + }
68
+ + public func toRawPixelDataAsync(allowGpu: Bool?) throws -> Promise<RawPixelData> { Promise.async { try self.toRawPixelData(allowGpu: allowGpu) } }
69
+ + public func toEncodedImageData(format: ImageFormat, quality: Double?) throws -> EncodedImageData {
70
+ + let type: CFString
71
+ + switch format { case .jpg: type = UTType.jpeg.identifier as CFString; case .png: type = UTType.png.identifier as CFString; case .heic: type = UTType.heic.identifier as CFString }
72
+ + let data = NSMutableData()
73
+ + guard let destination = CGImageDestinationCreateWithData(data, type, 1, nil) else { throw RuntimeError.error(withMessage: "Image encoder is unavailable") }
74
+ + CGImageDestinationAddImage(destination, cgImage, [kCGImageDestinationLossyCompressionQuality: max(0, min(100, quality ?? 100)) / 100] as CFDictionary)
75
+ + guard CGImageDestinationFinalize(destination) else { throw RuntimeError.error(withMessage: "Image encoding failed") }
76
+ + return EncodedImageData(buffer: try ArrayBuffer.copy(data: data as Data), width: width, height: height, imageFormat: format)
77
+ + }
78
+ + public func toEncodedImageDataAsync(format: ImageFormat, quality: Double?) throws -> Promise<EncodedImageData> { Promise.async { try self.toEncodedImageData(format: format, quality: quality) } }
79
+ + public func resize(width: Double, height: Double) throws -> any HybridImageSpec {
80
+ + try render(width, height) { $0.interpolationQuality = .high; $0.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height)) }
81
+ + }
82
+ + public func resizeAsync(width: Double, height: Double) throws -> Promise<any HybridImageSpec> { Promise.async { try self.resize(width: width, height: height) } }
83
+ + public func rotate(degrees: Double, allowFastFlagRotation: Bool?) throws -> any HybridImageSpec {
84
+ + guard degrees.isFinite else { throw RuntimeError.error(withMessage: "Invalid rotation") }
85
+ + let radians = degrees * .pi / 180
86
+ + let bounds = CGRect(x: 0, y: 0, width: width, height: height).applying(CGAffineTransform(rotationAngle: radians))
87
+ + return try render(abs(bounds.width), abs(bounds.height)) { context in
88
+ + context.translateBy(x: abs(bounds.width) / 2, y: abs(bounds.height) / 2); context.rotate(by: -radians)
89
+ + context.draw(cgImage, in: CGRect(x: -width / 2, y: -height / 2, width: width, height: height))
90
+ + }
91
+ + }
92
+ + public func rotateAsync(degrees: Double, allowFastFlagRotation: Bool?) throws -> Promise<any HybridImageSpec> { Promise.async { try self.rotate(degrees: degrees, allowFastFlagRotation: allowFastFlagRotation) } }
93
+ + public func crop(startX: Double, startY: Double, endX: Double, endY: Double) throws -> any HybridImageSpec {
94
+ + guard startX >= 0, startY >= 0, endX <= width, endY <= height, endX > startX, endY > startY,
95
+ + let image = cgImage.cropping(to: CGRect(x: startX, y: startY, width: endX - startX, height: endY - startY)) else { throw RuntimeError.error(withMessage: "Invalid crop rectangle") }
96
+ + return HybridCGImage(cgImage: image)
97
+ + }
98
+ + public func cropAsync(startX: Double, startY: Double, endX: Double, endY: Double) throws -> Promise<any HybridImageSpec> { Promise.async { try self.crop(startX: startX, startY: startY, endX: endX, endY: endY) } }
99
+ + public func mirrorHorizontally() throws -> any HybridImageSpec { try render(width, height) { $0.translateBy(x: width, y: 0); $0.scaleBy(x: -1, y: 1); $0.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height)) } }
100
+ + public func mirrorHorizontallyAsync() throws -> Promise<any HybridImageSpec> { Promise.async { try self.mirrorHorizontally() } }
101
+ + public func saveToFileAsync(path: String, format: ImageFormat, quality: Double?) throws -> Promise<Void> {
102
+ + Promise.async {
103
+ + let url = path.hasPrefix("file://") ? URL(string: path)! : URL(fileURLWithPath: path)
104
+ + try FileManager.default.createDirectory(at: url.deletingLastPathComponent(), withIntermediateDirectories: true)
105
+ + let encoded = try self.toEncodedImageData(format: format, quality: quality)
106
+ + try encoded.buffer.toData(copyIfNeeded: false).write(to: url, options: .atomic)
107
+ + }
108
+ + }
109
+ + public func saveToTemporaryFileAsync(format: ImageFormat, quality: Double?) throws -> Promise<String> {
110
+ + Promise.async {
111
+ + let path = FileManager.default.temporaryDirectory.appendingPathComponent("\(UUID()).\(format.stringValue)").path
112
+ + let encoded = try self.toEncodedImageData(format: format, quality: quality)
113
+ + try encoded.buffer.toData(copyIfNeeded: false).write(to: URL(fileURLWithPath: path), options: .atomic)
114
+ + return path
115
+ + }
116
+ + }
117
+ + public func toThumbHash() throws -> ArrayBuffer { throw RuntimeError.error(withMessage: "ThumbHash is not implemented in the macOS prototype") }
118
+ + public func toThumbHashAsync() throws -> Promise<ArrayBuffer> { Promise.async { try self.toThumbHash() } }
119
+ + public func renderInto(image: any HybridImageSpec, x: Double, y: Double, width: Double, height: Double) throws -> any HybridImageSpec {
120
+ + guard let target = image as? NativeImage else { throw RuntimeError.error(withMessage: "Expected a native macOS image") }
121
+ + return try render(image.width, image.height) { context in
122
+ + context.draw(target.cgImage, in: CGRect(x: 0, y: 0, width: image.width, height: image.height))
123
+ + context.draw(cgImage, in: CGRect(x: x, y: image.height - y - height, width: width, height: height))
124
+ + }
125
+ + }
126
+ + public func renderIntoAsync(image: any HybridImageSpec, x: Double, y: Double, width: Double, height: Double) throws -> Promise<any HybridImageSpec> { Promise.async { try self.renderInto(image: image, x: x, y: y, width: width, height: height) } }
127
+ +}
128
+ +
129
+ +final class HybridImageFactory: HybridImageFactorySpec {
130
+ + func createBlankImage(width: Double, height: Double, enableAlpha: Bool, fill: Color?) throws -> any HybridImageSpec {
131
+ + let context = try bitmap(width, height)
132
+ + context.setFillColor(red: fill?.r ?? 0, green: fill?.g ?? 0, blue: fill?.b ?? 0, alpha: enableAlpha ? (fill?.a ?? 0) : 1)
133
+ + context.fill(CGRect(x: 0, y: 0, width: width, height: height))
134
+ + return HybridCGImage(cgImage: context.makeImage()!)
135
+ + }
136
+ + func createBlankImageAsync(width: Double, height: Double, enableAlpha: Bool, fill: Color?) throws -> Promise<any HybridImageSpec> { Promise.async { try self.createBlankImage(width: width, height: height, enableAlpha: enableAlpha, fill: fill) } }
137
+ + func loadFromFile(filePath: String) throws -> any HybridImageSpec {
138
+ + let url = filePath.hasPrefix("file://") ? URL(string: filePath)! : URL(fileURLWithPath: filePath)
139
+ + return try decode(Data(contentsOf: url))
140
+ + }
141
+ + private func decode(_ data: Data) throws -> any HybridImageSpec {
142
+ + guard let source = CGImageSourceCreateWithData(data as CFData, nil), let image = CGImageSourceCreateImageAtIndex(source, 0, nil) else { throw RuntimeError.error(withMessage: "Failed to decode image") }
143
+ + let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any]
144
+ + let orientation = (properties?[kCGImagePropertyOrientation] as? NSNumber)?.int32Value ?? 1
145
+ + let oriented = CIImage(cgImage: image).oriented(forExifOrientation: orientation)
146
+ + guard let result = CIContext().createCGImage(oriented, from: oriented.extent) else { throw RuntimeError.error(withMessage: "Failed to orient image") }
147
+ + return HybridCGImage(cgImage: result)
148
+ + }
149
+ + func loadFromFileAsync(filePath: String) throws -> Promise<any HybridImageSpec> { Promise.async { try self.loadFromFile(filePath: filePath) } }
150
+ + func loadFromResources(name: String) throws -> any HybridImageSpec {
151
+ + guard let url = Bundle.main.url(forResource: name, withExtension: nil) else { throw RuntimeError.error(withMessage: "Image resource not found: \(name)") }
152
+ + return try loadFromFile(filePath: url.path)
153
+ + }
154
+ + func loadFromResourcesAsync(name: String) throws -> Promise<any HybridImageSpec> { Promise.async { try self.loadFromResources(name: name) } }
155
+ + func loadFromSymbol(symbolName: String) throws -> any HybridImageSpec {
156
+ + guard let image = NSImage(systemSymbolName: symbolName, accessibilityDescription: nil), let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { throw RuntimeError.error(withMessage: "Symbol not found: \(symbolName)") }
157
+ + return HybridCGImage(cgImage: cgImage)
158
+ + }
159
+ + func loadFromRawPixelData(data: RawPixelData, allowGpu: Bool?) throws -> any HybridImageSpec {
160
+ + guard data.pixelFormat == .rgba, data.width > 0, data.height > 0, data.width <= 32768, data.height <= 32768, data.width.rounded() == data.width, data.height.rounded() == data.height else { throw RuntimeError.error(withMessage: "The macOS prototype accepts RGBA pixel data with integer dimensions") }
161
+ + let bytes = data.buffer.toData(copyIfNeeded: true)
162
+ + guard bytes.count == Int(data.width) * Int(data.height) * 4,
163
+ + let provider = CGDataProvider(data: bytes as CFData),
164
+ + let image = CGImage(width: Int(data.width), height: Int(data.height), bitsPerComponent: 8, bitsPerPixel: 32, bytesPerRow: Int(data.width) * 4, space: CGColorSpace(name: CGColorSpace.sRGB)!, bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue), provider: provider, decode: nil, shouldInterpolate: true, intent: .defaultIntent) else { throw RuntimeError.error(withMessage: "Invalid raw pixel buffer") }
165
+ + return HybridCGImage(cgImage: image)
166
+ + }
167
+ + func loadFromRawPixelDataAsync(data: RawPixelData, allowGpu: Bool?) throws -> Promise<any HybridImageSpec> { Promise.async { try self.loadFromRawPixelData(data: data, allowGpu: allowGpu) } }
168
+ + func loadFromEncodedImageData(data: EncodedImageData) throws -> any HybridImageSpec { try decode(data.buffer.toData(copyIfNeeded: true)) }
169
+ + func loadFromEncodedImageDataAsync(data: EncodedImageData) throws -> Promise<any HybridImageSpec> { Promise.async { try self.loadFromEncodedImageData(data: data) } }
170
+ + func loadFromThumbHash(thumbhash: ArrayBuffer) throws -> any HybridImageSpec { throw RuntimeError.error(withMessage: "ThumbHash is not implemented in the macOS prototype") }
171
+ + func loadFromThumbHashAsync(thumbhash: ArrayBuffer) throws -> Promise<any HybridImageSpec> { Promise.async { try self.loadFromThumbHash(thumbhash: thumbhash) } }
172
+ +}
173
+ +
174
+ +final class HybridImageUtils: HybridImageUtilsSpec {
175
+ + var supportsHeicLoading: Bool { (CGImageSourceCopyTypeIdentifiers() as! [String]).contains(UTType.heic.identifier) }
176
+ + var supportsHeicWriting: Bool { (CGImageDestinationCopyTypeIdentifiers() as! [String]).contains(UTType.heic.identifier) }
177
+ + func thumbHashToBase64String(thumbhash: ArrayBuffer) throws -> String { thumbhash.toData(copyIfNeeded: false).base64EncodedString() }
178
+ + func thumbhashFromBase64String(thumbhashBase64: String) throws -> ArrayBuffer {
179
+ + guard let data = Data(base64Encoded: thumbhashBase64) else { throw RuntimeError.error(withMessage: "Invalid base64") }
180
+ + return try ArrayBuffer.copy(data: data)
181
+ + }
182
+ +}
183
+ +
184
+ +final class HybridImageLoader: HybridImageLoaderSpec {
185
+ + let load: () throws -> any HybridImageSpec
186
+ + init(load: @escaping () throws -> any HybridImageSpec) { self.load = load; super.init() }
187
+ + func loadImage() throws -> Promise<any HybridImageSpec> { Promise.async { try self.load() } }
188
+ + func requestImage(forView: any HybridNitroImageViewSpec) throws { DispatchQueue.main.async { (forView as? HybridImageView)?.load(self) } }
189
+ + func dropImage(forView: any HybridNitroImageViewSpec) throws { DispatchQueue.main.async { (forView as? HybridImageView)?.clear() } }
190
+ +}
191
+ +final class HybridImageLoaderFactory: HybridImageLoaderFactorySpec {
192
+ + let factory = HybridImageFactory()
193
+ + func createFileImageLoader(filePath: String) throws -> any HybridImageLoaderSpec { HybridImageLoader { try self.factory.loadFromFile(filePath: filePath) } }
194
+ + func createResourceImageLoader(name: String) throws -> any HybridImageLoaderSpec { HybridImageLoader { try self.factory.loadFromResources(name: name) } }
195
+ + func createSymbolImageLoader(symbolName: String) throws -> any HybridImageLoaderSpec { HybridImageLoader { try self.factory.loadFromSymbol(symbolName: symbolName) } }
196
+ + func createRawPixelDataImageLoader(data: RawPixelData) throws -> any HybridImageLoaderSpec { HybridImageLoader { try self.factory.loadFromRawPixelData(data: data, allowGpu: false) } }
197
+ + func createEncodedImageDataImageLoader(data: EncodedImageData) throws -> any HybridImageLoaderSpec { HybridImageLoader { try self.factory.loadFromEncodedImageData(data: data) } }
198
+ +}
199
+ +final class HybridImageView: HybridNitroImageViewSpec {
200
+ + let view = NSView()
201
+ + private var generation = 0
202
+ + var resizeMode: ResizeMode? { didSet { view.layer?.contentsGravity = resizeMode == .contain ? .resizeAspect : resizeMode == .stretch ? .resize : resizeMode == .center ? .center : .resizeAspectFill } }
203
+ + var recyclingKey: String? { didSet { if recyclingKey != oldValue { clear() } } }
204
+ + var image: Variant__any_HybridImageSpec___any_HybridImageLoaderSpec_? { didSet {
205
+ + clear()
206
+ + switch image { case .first(let image): view.layer?.contents = (image as? NativeImage)?.cgImage; case .second(let loader): load(loader); case nil: break }
207
+ + } }
208
+ + override init() { super.init(); view.wantsLayer = true; view.layer?.masksToBounds = true; view.layer?.contentsGravity = .resizeAspectFill }
209
+ + func clear() { generation += 1; view.layer?.contents = nil }
210
+ + func load(_ loader: any HybridImageLoaderSpec) {
211
+ + generation += 1; let token = generation
212
+ + do { try loader.loadImage().then { [weak self] image in DispatchQueue.main.async { guard let self, self.generation == token else { return }; self.view.layer?.contents = (image as? NativeImage)?.cgImage } }.catch { [weak self] _ in DispatchQueue.main.async { guard let self, self.generation == token else { return }; self.clear() } } } catch { clear() }
213
+ + }
214
+ + func onDropView() { clear() }
215
+ +}
216
+ diff --git a/nitrogen/generated/ios/NitroImage+autolinking.rb b/nitrogen/generated/ios/NitroImage+autolinking.rb
217
+ index 06be8b7..2114874 100644
218
+ --- a/nitrogen/generated/ios/NitroImage+autolinking.rb
219
+ +++ b/nitrogen/generated/ios/NitroImage+autolinking.rb
220
+ @@ -54,6 +54,8 @@ def add_nitrogen_files(spec)
221
+ "CLANG_CXX_LANGUAGE_STANDARD" => "c++20",
222
+ # Enables C++ <-> Swift interop (by default it's only ObjC)
223
+ "SWIFT_OBJC_INTEROP_MODE" => "objcxx",
224
+ + # Nitro's Fabric-only templates must not enter its Swift umbrella.
225
+ + "HEADER_SEARCH_PATHS" => "$(inherited) $(PODS_ROOT)/Headers/Private",
226
+ # Enables stricter modular headers
227
+ "DEFINES_MODULE" => "YES",
228
+ # Disable auto-generated ObjC header for Swift (Static linkage on Xcode 26.4 breaks here)
229
+ diff --git a/nitrogen/generated/ios/c++/views/HybridNitroImageViewComponent.mm b/nitrogen/generated/ios/c++/views/HybridNitroImageViewComponent.mm
230
+ index 34ef3c1..b45a8b2 100644
231
+ --- a/nitrogen/generated/ios/c++/views/HybridNitroImageViewComponent.mm
232
+ +++ b/nitrogen/generated/ios/c++/views/HybridNitroImageViewComponent.mm
233
+ @@ -12,7 +12,12 @@
234
+ #import <React/RCTComponentViewFactory.h>
235
+ #import <React/UIView+ComponentViewProtocol.h>
236
+ #import <NitroModules/NitroDefines.hpp>
237
+ +#import <TargetConditionals.h>
238
+ +#if TARGET_OS_OSX
239
+ +#import <AppKit/AppKit.h>
240
+ +#else
241
+ #import <UIKit/UIKit.h>
242
+ +#endif
243
+
244
+ #import "HybridNitroImageViewSpecSwift.hpp"
245
+ #import "NitroImage-Swift-Cxx-Umbrella.hpp"
246
+ @@ -65,12 +70,21 @@ - (void) updateView {
247
+
248
+ // 2. Get UIView*
249
+ void* viewUnsafe = swiftPart.getView();
250
+ + #if TARGET_OS_OSX
251
+ + NSView* view = (__bridge_transfer NSView*) viewUnsafe;
252
+ +#else
253
+ UIView* view = (__bridge_transfer UIView*) viewUnsafe;
254
+ +#endif
255
+
256
+ // 3. Update RCTViewComponentView's [contentView]
257
+ [self setContentView:view];
258
+ }
259
+
260
+ +// RN < 0.82 does not call invalidate for views which opt out of recycling.
261
+ +- (void) dealloc {
262
+ + if (_hybridView != nullptr) [self notifyOnDropView];
263
+ +}
264
+ +
265
+ - (void) notifyOnDropView {
266
+ // A recycled component can later be invalidated. Notify only once per mount.
267
+ if (_didDropView) {
268
+ diff --git a/nitrogen/generated/shared/c++/views/HybridNitroImageViewComponent.cpp b/nitrogen/generated/shared/c++/views/HybridNitroImageViewComponent.cpp
269
+ index a65f0aa..1de5350 100644
270
+ --- a/nitrogen/generated/shared/c++/views/HybridNitroImageViewComponent.cpp
271
+ +++ b/nitrogen/generated/shared/c++/views/HybridNitroImageViewComponent.cpp
272
+ @@ -7,6 +7,9 @@
273
+
274
+ #include "HybridNitroImageViewComponent.hpp"
275
+
276
+ +#if defined(__APPLE__)
277
+ +#include <TargetConditionals.h>
278
+ +#endif
279
+ #include <NitroModules/NitroHash.hpp>
280
+ #include <NitroModules/ReactProp.hpp>
281
+
282
+ @@ -19,7 +22,11 @@ namespace margelo::nitro::image::views {
283
+ HybridNitroImageViewProps::HybridNitroImageViewProps(const react::PropsParserContext& context,
284
+ const HybridNitroImageViewProps& sourceProps,
285
+ const react::RawProps& rawProps):
286
+ - react::ViewProps(context, sourceProps, rawProps, filterObjectKeys),
287
+ + react::ViewProps(context, sourceProps, rawProps
288
+ + #if !TARGET_OS_OSX
289
+ + , filterObjectKeys
290
+ + #endif
291
+ + ),
292
+ image(nitro::ReactProp<std::optional<std::variant<std::shared_ptr<HybridImageSpec>, std::shared_ptr<HybridImageLoaderSpec>>>>::fromRawValue("NitroImageView", "image", rawProps, sourceProps.image)),
293
+ resizeMode(nitro::ReactProp<std::optional<ResizeMode>>::fromRawValue("NitroImageView", "resizeMode", rawProps, sourceProps.resizeMode)),
294
+ recyclingKey(nitro::ReactProp<std::optional<std::string>>::fromRawValue("NitroImageView", "recyclingKey", rawProps, sourceProps.recyclingKey)),
295
+ diff --git a/package.json b/package.json
296
+ index f22fe12..94be214 100644
297
+ --- a/package.json
298
+ +++ b/package.json
299
+ @@ -28,7 +28,8 @@
300
+ "app.plugin.js",
301
+ "nitro.json",
302
+ "*.podspec",
303
+ - "README.md"
304
+ + "README.md",
305
+ + "macos/**/*.swift"
306
+ ],
307
+ "scripts": {
308
+ "build": "rm -rf tsconfig.tsbuildinfo && rm -rf lib && bun typecheck && bob build",
309
+ diff --git a/react-native.config.js b/react-native.config.js
310
+ index 3fdf8ea..9c4bc69 100644
311
+ --- a/react-native.config.js
312
+ +++ b/react-native.config.js
313
+ @@ -7,6 +7,7 @@ module.exports = {
314
+ * @type {import('@react-native-community/cli-types').IOSDependencyParams}
315
+ */
316
+ ios: {},
317
+ + macos: { podspecPath: __dirname + "/NitroImage.podspec" },
318
+ /**
319
+ * @type {import('@react-native-community/cli-types').AndroidDependencyParams}
320
+ */
@@ -0,0 +1,50 @@
1
+ diff --git a/NitroModules.podspec b/NitroModules.podspec
2
+ index 9163edc..5067d95 100644
3
+ --- a/NitroModules.podspec
4
+ +++ b/NitroModules.podspec
5
+ @@ -51,10 +51,6 @@ Pod::Spec.new do |s|
6
+ "cpp/utils/NitroDefines.hpp",
7
+ "cpp/utils/PropNameIDCache.hpp",
8
+ "cpp/views/CachedProp.hpp",
9
+ - "cpp/views/RawPropsCompat.hpp",
10
+ - "cpp/views/ReactProp.hpp",
11
+ - "cpp/views/ViewComponentDescriptor.hpp",
12
+ - "cpp/views/ViewPropsHolderState.hpp",
13
+ # Public iOS-specific headers that will be exposed in modulemap (for Swift)
14
+ "ios/core/ArrayBufferHolder.hpp",
15
+ "ios/core/PromiseHolder.hpp",
16
+ @@ -65,6 +61,9 @@ Pod::Spec.new do |s|
17
+ "ios/utils/SwiftClosure.hpp",
18
+ ]
19
+
20
+ + # These templates are consumed by generated C++ views, not Swift.
21
+ + s.private_header_files = ["cpp/views/RawPropsCompat.hpp", "cpp/views/ReactProp.hpp", "cpp/views/ViewComponentDescriptor.hpp", "cpp/views/ViewPropsHolderState.hpp"]
22
+ +
23
+ xcconfig = {
24
+ # Use C++ 20
25
+ "CLANG_CXX_LANGUAGE_STANDARD" => "c++20",
26
+ diff --git a/ios/views/HybridView.swift b/ios/views/HybridView.swift
27
+ index 94a422e..88928fd 100644
28
+ --- a/ios/views/HybridView.swift
29
+ +++ b/ios/views/HybridView.swift
30
+ @@ -6,12 +6,18 @@
31
+ //
32
+
33
+ #if canImport(UIKit)
34
+ +import UIKit
35
+ +public typealias NitroPlatformView = UIView
36
+ +#elseif canImport(AppKit)
37
+ +import AppKit
38
+ +public typealias NitroPlatformView = NSView
39
+ +#endif
40
+
41
+ - import UIKit
42
+ +#if canImport(UIKit) || canImport(AppKit)
43
+
44
+ /// A base protocol for all Swift-based Hybrid Views.
45
+ public protocol HybridView: HybridObject {
46
+ - associatedtype ViewType: UIView
47
+ + associatedtype ViewType: NitroPlatformView
48
+ /**
49
+ * Get the ``UIView`` this HybridView is holding.
50
+ *