@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.
- package/camera.ts +13 -0
- package/package.json +21 -11
- package/patches/camera/react-native-nitro-image.patch +320 -0
- package/patches/camera/react-native-nitro-modules.patch +50 -0
- package/patches/camera/react-native-vision-camera.patch +2662 -0
- package/patches/camera/upstream.json +22 -0
- package/patches/dependencies.json +8 -0
- package/patches/react-native-enriched-markdown@1.0.2.patch +8684 -0
- package/postinstall.cjs +1 -0
- package/ui-swipe.ts +1 -0
- package/vendor/node_modules/@legendapp/spark-ai/package.json +3 -3
- package/vendor/node_modules/@legendapp/spark-audio/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-auth-session/package.json +3 -3
- package/vendor/node_modules/@legendapp/spark-cli/dist/build.js +62 -11
- package/vendor/node_modules/@legendapp/spark-cli/dist/cli-arguments.js +2 -1
- package/vendor/node_modules/@legendapp/spark-cli/dist/commands.js +3 -2
- package/vendor/node_modules/@legendapp/spark-cli/dist/create.js +6 -3
- package/vendor/node_modules/@legendapp/spark-cli/dist/credentials.js +41 -26
- package/vendor/node_modules/@legendapp/spark-cli/dist/dependency-pins.js +132 -0
- package/vendor/node_modules/@legendapp/spark-cli/dist/dev-command.js +1 -1
- package/vendor/node_modules/@legendapp/spark-cli/dist/dev.js +1 -1
- package/vendor/node_modules/@legendapp/spark-cli/dist/errors.js +100 -0
- package/vendor/node_modules/@legendapp/spark-cli/dist/examples.js +1 -1
- package/vendor/node_modules/@legendapp/spark-cli/dist/expo-native.cjs +5 -1
- package/vendor/node_modules/@legendapp/spark-cli/dist/helpers.js +1 -1
- package/vendor/node_modules/@legendapp/spark-cli/dist/index.js +43 -7
- package/vendor/node_modules/@legendapp/spark-cli/dist/json.js +16 -0
- package/vendor/node_modules/@legendapp/spark-cli/dist/native-patch-preflight.js +3 -1
- package/vendor/node_modules/@legendapp/spark-cli/dist/process.js +7 -0
- package/vendor/node_modules/@legendapp/spark-cli/dist/project.js +27 -22
- package/vendor/node_modules/@legendapp/spark-cli/dist/release.js +2 -1
- package/vendor/node_modules/@legendapp/spark-cli/dist/release.json +19 -19
- package/vendor/node_modules/@legendapp/spark-cli/dist/universal.cjs +2 -1
- package/vendor/node_modules/@legendapp/spark-cli/dist/windows.js +1 -8
- package/vendor/node_modules/@legendapp/spark-cli/package.json +3 -3
- package/vendor/node_modules/@legendapp/spark-cli/templates/blank-typescript/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-cli/templates/universal/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-cli/templates/windows/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-clipboard/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-codex/package.json +3 -3
- package/vendor/node_modules/@legendapp/spark-commands/package.json +5 -5
- package/vendor/node_modules/@legendapp/spark-context-menu/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-desktop-app/package.json +1 -1
- package/vendor/node_modules/@legendapp/spark-desktop-app/src/contracts/menu.ts +6 -4
- package/vendor/node_modules/@legendapp/spark-desktop-config/config.cjs +2 -0
- package/vendor/node_modules/@legendapp/spark-desktop-config/expo.cjs +1 -0
- package/vendor/node_modules/@legendapp/spark-desktop-config/native-patches.cjs +106 -0
- package/vendor/node_modules/@legendapp/spark-desktop-config/native-patches.d.cts +1 -0
- package/vendor/node_modules/@legendapp/spark-desktop-config/package.json +5 -4
- package/vendor/node_modules/@legendapp/spark-desktop-host/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-desktop-links/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-desktop-shortcuts/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-desktop-windows/macos/window-manager/RNWindowManager.mm +39 -0
- package/vendor/node_modules/@legendapp/spark-desktop-windows/package.json +3 -3
- package/vendor/node_modules/@legendapp/spark-desktop-windows/src/api.ts +3 -1
- package/vendor/node_modules/@legendapp/spark-desktop-windows/src/macos-adapter.ts +1 -1
- package/vendor/node_modules/@legendapp/spark-documents/package.json +7 -7
- package/vendor/node_modules/@legendapp/spark-drag-drop/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-file-dialog/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-file-system/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-global-shortcuts/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-message-dialog/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-native-menu/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-notifications/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-notifications/windows/SparkNotifications/SparkNotifications.h +16 -11
- package/vendor/node_modules/@legendapp/spark-processes/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-secure-storage/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-settings/package.json +3 -3
- package/vendor/node_modules/@legendapp/spark-settings-ui/package.json +4 -4
- package/vendor/node_modules/@legendapp/spark-sqlite/package.json +3 -3
- package/vendor/node_modules/@legendapp/spark-system/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-tray/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-ui/macos/appkit-split-view/RNSidebarSplitViewComponent.mm +263 -26
- package/vendor/node_modules/@legendapp/spark-ui/macos/swipe-actions/RNSwipeActions.h +10 -0
- package/vendor/node_modules/@legendapp/spark-ui/macos/swipe-actions/RNSwipeActions.mm +422 -0
- package/vendor/node_modules/@legendapp/spark-ui/package.json +3 -2
- package/vendor/node_modules/@legendapp/spark-ui/src/appkit-split-view/SidebarSplitViewNativeComponent.ts +6 -0
- package/vendor/node_modules/@legendapp/spark-ui/src/appkit-split-view/index.tsx +20 -11
- package/vendor/node_modules/@legendapp/spark-ui/src/swipe-actions/SwipeActionsNativeComponent.ts +11 -0
- package/vendor/node_modules/@legendapp/spark-ui/src/swipe-actions/index.tsx +41 -0
- package/vendor/node_modules/@legendapp/spark-updates/package.json +2 -2
- package/vendor/node_modules/@legendapp/spark-webview/package.json +2 -2
- 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
|
+
"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.
|
|
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.
|
|
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": "
|
|
177
|
+
"patchHash": "985164a87eb04010a8d849f5b0ca22a76548dc3f73a64a6183683ca90932ec2c",
|
|
168
178
|
"upstreamRevision": "58710c25c6e505dcc1292ee54d855408a6f7a42d"
|
|
169
179
|
},
|
|
170
180
|
"react-native-nitro-modules": {
|
|
171
|
-
"version": "0.
|
|
172
|
-
"patchHash": "
|
|
173
|
-
"upstreamIntegrity": "sha512-
|
|
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": "
|
|
187
|
+
"patchHash": "d535dcd20aa0f5bcf8860086eb6484c73b1e84d8a586ea9fe74dca76085955a0",
|
|
178
188
|
"upstreamIntegrity": "sha512-atVOtXyFzO0e454eVS27AMwRs515URCKrkvLLG+mGKcyiF5UPfC53oukpNp2UmVzlLebyTEXNkTYs6n0dVGd0g=="
|
|
179
189
|
},
|
|
180
190
|
"react-native-webview": {
|
|
181
191
|
"version": "16.0.0",
|
|
182
|
-
"patchHash": "
|
|
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
|
+
*
|