aix 0.5.1 → 0.6.1
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/ios/EditMenuDefaultActions.swift +70 -0
- package/ios/HybridAix.swift +28 -3
- package/ios/HybridAixCellView.swift +0 -1
- package/ios/HybridAixComposer.swift +82 -0
- package/ios/HybridAixDropzone.swift +104 -0
- package/ios/HybridAixInputWrapper.swift +447 -0
- package/ios/InputType.swift +40 -0
- package/ios/PasteFileManager.swift +92 -0
- package/nitro.json +8 -0
- package/nitrogen/generated/android/Aix+autolinking.cmake +8 -0
- package/nitrogen/generated/android/AixOnLoad.cpp +26 -0
- package/nitrogen/generated/android/c++/JAixInputWrapperOnPasteEvent.hpp +70 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_AixInputWrapperOnPasteEvent_.hpp +98 -0
- package/nitrogen/generated/android/c++/JHybridAixComposerSpec.cpp +9 -0
- package/nitrogen/generated/android/c++/JHybridAixComposerSpec.hpp +2 -0
- package/nitrogen/generated/android/c++/JHybridAixDropzoneSpec.cpp +72 -0
- package/nitrogen/generated/android/c++/JHybridAixDropzoneSpec.hpp +66 -0
- package/nitrogen/generated/android/c++/JHybridAixInputWrapperSpec.cpp +144 -0
- package/nitrogen/generated/android/c++/JHybridAixInputWrapperSpec.hpp +74 -0
- package/nitrogen/generated/android/c++/views/JHybridAixComposerStateUpdater.cpp +4 -0
- package/nitrogen/generated/android/c++/views/JHybridAixDropzoneStateUpdater.cpp +56 -0
- package/nitrogen/generated/android/c++/views/JHybridAixDropzoneStateUpdater.hpp +49 -0
- package/nitrogen/generated/android/c++/views/JHybridAixInputWrapperStateUpdater.cpp +72 -0
- package/nitrogen/generated/android/c++/views/JHybridAixInputWrapperStateUpdater.hpp +49 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/AixInputWrapperOnPasteEvent.kt +47 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/Func_void_std__vector_AixInputWrapperOnPasteEvent_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/HybridAixComposerSpec.kt +6 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/HybridAixDropzoneSpec.kt +67 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/HybridAixInputWrapperSpec.kt +91 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/views/HybridAixDropzoneManager.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/views/HybridAixDropzoneStateUpdater.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/views/HybridAixInputWrapperManager.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/views/HybridAixInputWrapperStateUpdater.kt +23 -0
- package/nitrogen/generated/ios/Aix-Swift-Cxx-Bridge.cpp +42 -0
- package/nitrogen/generated/ios/Aix-Swift-Cxx-Bridge.hpp +112 -0
- package/nitrogen/generated/ios/Aix-Swift-Cxx-Umbrella.hpp +14 -0
- package/nitrogen/generated/ios/AixAutolinking.mm +16 -0
- package/nitrogen/generated/ios/AixAutolinking.swift +30 -0
- package/nitrogen/generated/ios/c++/HybridAixComposerSpecSwift.hpp +7 -0
- package/nitrogen/generated/ios/c++/HybridAixDropzoneSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridAixDropzoneSpecSwift.hpp +80 -0
- package/nitrogen/generated/ios/c++/HybridAixInputWrapperSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridAixInputWrapperSpecSwift.hpp +108 -0
- package/nitrogen/generated/ios/c++/views/HybridAixComposerComponent.mm +5 -0
- package/nitrogen/generated/ios/c++/views/HybridAixDropzoneComponent.mm +96 -0
- package/nitrogen/generated/ios/c++/views/HybridAixInputWrapperComponent.mm +116 -0
- package/nitrogen/generated/ios/swift/AixInputWrapperOnPasteEvent.swift +107 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_AixInputWrapperOnPasteEvent_.swift +47 -0
- package/nitrogen/generated/ios/swift/HybridAixComposerSpec.swift +1 -0
- package/nitrogen/generated/ios/swift/HybridAixComposerSpec_cxx.swift +24 -0
- package/nitrogen/generated/ios/swift/HybridAixDropzoneSpec.swift +56 -0
- package/nitrogen/generated/ios/swift/HybridAixDropzoneSpec_cxx.swift +167 -0
- package/nitrogen/generated/ios/swift/HybridAixInputWrapperSpec.swift +60 -0
- package/nitrogen/generated/ios/swift/HybridAixInputWrapperSpec_cxx.swift +261 -0
- package/nitrogen/generated/shared/c++/AixInputWrapperOnPasteEvent.hpp +88 -0
- package/nitrogen/generated/shared/c++/HybridAixComposerSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridAixComposerSpec.hpp +2 -0
- package/nitrogen/generated/shared/c++/HybridAixDropzoneSpec.cpp +22 -0
- package/nitrogen/generated/shared/c++/HybridAixDropzoneSpec.hpp +67 -0
- package/nitrogen/generated/shared/c++/HybridAixInputWrapperSpec.cpp +30 -0
- package/nitrogen/generated/shared/c++/HybridAixInputWrapperSpec.hpp +76 -0
- package/nitrogen/generated/shared/c++/views/HybridAixComposerComponent.cpp +12 -0
- package/nitrogen/generated/shared/c++/views/HybridAixComposerComponent.hpp +1 -0
- package/nitrogen/generated/shared/c++/views/HybridAixDropzoneComponent.cpp +87 -0
- package/nitrogen/generated/shared/c++/views/HybridAixDropzoneComponent.hpp +109 -0
- package/nitrogen/generated/shared/c++/views/HybridAixInputWrapperComponent.cpp +135 -0
- package/nitrogen/generated/shared/c++/views/HybridAixInputWrapperComponent.hpp +114 -0
- package/nitrogen/generated/shared/json/AixComposerConfig.json +1 -0
- package/nitrogen/generated/shared/json/AixDropzoneConfig.json +10 -0
- package/nitrogen/generated/shared/json/AixInputWrapperConfig.json +14 -0
- package/package.json +1 -1
- package/src/dropzone.ios.tsx +27 -0
- package/src/dropzone.tsx +10 -0
- package/src/index.ts +3 -0
- package/src/input-wrapper.ios.tsx +30 -0
- package/src/input-wrapper.tsx +17 -0
- package/src/views/aix.nitro.ts +33 -19
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import UIKit
|
|
3
|
+
import UniformTypeIdentifiers
|
|
4
|
+
import ObjectiveC.runtime
|
|
5
|
+
|
|
6
|
+
private let LOG_TAG = "[AixInputWrapper]"
|
|
7
|
+
|
|
8
|
+
fileprivate enum NativeIDKey: String {
|
|
9
|
+
case textInput
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
private var allowedActionsKey: UInt8 = 0
|
|
13
|
+
|
|
14
|
+
class HybridAixInputWrapper: HybridAixInputWrapperSpec {
|
|
15
|
+
|
|
16
|
+
// MARK: - Props
|
|
17
|
+
var pasteConfiguration: [String]?
|
|
18
|
+
var editMenuDefaultActions: [String]? {
|
|
19
|
+
didSet {
|
|
20
|
+
self.parsedEditMenuActions = editMenuDefaultActions?
|
|
21
|
+
.compactMap { EditMenuDefaultActions(rawValue: $0) }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
var maxLines: Double?
|
|
25
|
+
var maxChars: Double?
|
|
26
|
+
var onPaste: ((_ events: [AixInputWrapperOnPasteEvent]) -> Void)?
|
|
27
|
+
|
|
28
|
+
// MARK: - Private State
|
|
29
|
+
private var parsedEditMenuActions: [EditMenuDefaultActions]?
|
|
30
|
+
private var wrappedTextInput: InputType?
|
|
31
|
+
private let dropDelegate = ImageDropDelegate()
|
|
32
|
+
private lazy var pasteDelegate = PasteHandlerDelegate(owner: self)
|
|
33
|
+
|
|
34
|
+
// MARK: - Inner View
|
|
35
|
+
private final class InnerView: UIView {
|
|
36
|
+
weak var owner: HybridAixInputWrapper?
|
|
37
|
+
|
|
38
|
+
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
override func didMoveToSuperview() {
|
|
43
|
+
super.didMoveToSuperview()
|
|
44
|
+
scheduleAttach()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
override func didAddSubview(_ subview: UIView) {
|
|
48
|
+
super.didAddSubview(subview)
|
|
49
|
+
scheduleAttach()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
override func layoutSubviews() {
|
|
53
|
+
super.layoutSubviews()
|
|
54
|
+
scheduleAttach()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private func scheduleAttach() {
|
|
58
|
+
guard owner?.wrappedTextInput == nil else { return }
|
|
59
|
+
DispatchQueue.main.async { [weak self] in
|
|
60
|
+
self?.owner?.attachIfPossible()
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
override func willMove(toSuperview newSuperview: UIView?) {
|
|
65
|
+
super.willMove(toSuperview: newSuperview)
|
|
66
|
+
if newSuperview == nil {
|
|
67
|
+
owner?.wrappedTextInput = nil
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
let view: UIView
|
|
73
|
+
|
|
74
|
+
// MARK: - Init
|
|
75
|
+
override init() {
|
|
76
|
+
let inner = InnerView()
|
|
77
|
+
self.view = inner
|
|
78
|
+
super.init()
|
|
79
|
+
inner.owner = self
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// MARK: - Paste Handling
|
|
83
|
+
func handlePaste() {
|
|
84
|
+
print("\(LOG_TAG) Paste triggered")
|
|
85
|
+
PasteFileManager.cleanupOldFiles()
|
|
86
|
+
let pb = UIPasteboard.general
|
|
87
|
+
|
|
88
|
+
if let images = pb.images, images.count > 0 {
|
|
89
|
+
print("\(LOG_TAG) Pasting \(images.count) image(s)")
|
|
90
|
+
saveImages(images) { [weak self] events in
|
|
91
|
+
if !events.isEmpty {
|
|
92
|
+
self?.onPaste?(events)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} else if let url = pb.url {
|
|
96
|
+
Task { [weak self] in
|
|
97
|
+
if let image = await self?.fetchImage(from: url) {
|
|
98
|
+
self?.saveImages([image]) { events in
|
|
99
|
+
if !events.isEmpty {
|
|
100
|
+
self?.onPaste?(events)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} else if !pb.itemProviders.isEmpty {
|
|
106
|
+
checkForFiles(itemProviders: pb.itemProviders) { [weak self] events in
|
|
107
|
+
if !events.isEmpty {
|
|
108
|
+
self?.onPaste?(events)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
func saveImages(_ images: [UIImage], completion: @escaping ([AixInputWrapperOnPasteEvent]) -> Void) {
|
|
115
|
+
var events: [AixInputWrapperOnPasteEvent] = []
|
|
116
|
+
for image in images {
|
|
117
|
+
if let uri = try? PasteFileManager.save(image: image) {
|
|
118
|
+
let fileURL = URL(fileURLWithPath: uri)
|
|
119
|
+
let ext = fileURL.pathExtension
|
|
120
|
+
print("\(LOG_TAG) Image saved to \(uri)")
|
|
121
|
+
events.append(AixInputWrapperOnPasteEvent(
|
|
122
|
+
type: "image",
|
|
123
|
+
filePath: uri,
|
|
124
|
+
fileExtension: ext,
|
|
125
|
+
fileName: fileURL.lastPathComponent
|
|
126
|
+
))
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
completion(events)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// MARK: - Text Input Attachment
|
|
133
|
+
func attachIfPossible() {
|
|
134
|
+
guard wrappedTextInput == nil else { return }
|
|
135
|
+
let searchRoot = view.superview ?? view
|
|
136
|
+
guard let input = findTextInput(in: searchRoot) else {
|
|
137
|
+
print("\(LOG_TAG) No text input found in subview hierarchy")
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let onImageHandler: (UIImage) -> Void = { [weak self] image in
|
|
142
|
+
self?.saveImages([image]) { events in
|
|
143
|
+
if !events.isEmpty {
|
|
144
|
+
self?.onPaste?(events)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
dropDelegate.onImage = onImageHandler
|
|
149
|
+
|
|
150
|
+
(view as? InnerView).map { $0.addInteraction(UIDropInteraction(delegate: dropDelegate)) }
|
|
151
|
+
|
|
152
|
+
let acceptableTypes = [
|
|
153
|
+
UTType.image.identifier,
|
|
154
|
+
UTType.pdf.identifier,
|
|
155
|
+
UTType.svg.identifier,
|
|
156
|
+
UTType.url.identifier,
|
|
157
|
+
UTType.text.identifier
|
|
158
|
+
]
|
|
159
|
+
|
|
160
|
+
if let tf = input as? UITextField {
|
|
161
|
+
print("\(LOG_TAG) Found UITextField, attaching paste delegate")
|
|
162
|
+
self.wrappedTextInput = .init(textField: tf)
|
|
163
|
+
tf.pasteConfiguration = .init(acceptableTypeIdentifiers: acceptableTypes)
|
|
164
|
+
tf.pasteDelegate = pasteDelegate
|
|
165
|
+
tf.addInteraction(UIDropInteraction(delegate: dropDelegate))
|
|
166
|
+
applyEditMenuFilter(to: tf)
|
|
167
|
+
} else if let tv = input as? UITextView {
|
|
168
|
+
print("\(LOG_TAG) Found UITextView, attaching paste delegate")
|
|
169
|
+
self.wrappedTextInput = .init(textView: tv)
|
|
170
|
+
tv.pasteConfiguration = .init(acceptableTypeIdentifiers: acceptableTypes)
|
|
171
|
+
tv.pasteDelegate = pasteDelegate
|
|
172
|
+
tv.addInteraction(UIDropInteraction(delegate: dropDelegate))
|
|
173
|
+
applyEditMenuFilter(to: tv)
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private func findTextInput(in root: UIView) -> UIView? {
|
|
178
|
+
if root is UITextField { return root }
|
|
179
|
+
if root is UITextView { return root }
|
|
180
|
+
for sub in root.subviews {
|
|
181
|
+
if let found = findTextInput(in: sub) { return found }
|
|
182
|
+
}
|
|
183
|
+
return nil
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private func applyEditMenuFilter(to input: UIView) {
|
|
187
|
+
guard let actions = parsedEditMenuActions else { return }
|
|
188
|
+
let allowedSelectors = actions.map { $0.selector }
|
|
189
|
+
objc_setAssociatedObject(input, &allowedActionsKey, allowedSelectors, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
|
|
190
|
+
swizzleCanPerformAction(on: input)
|
|
191
|
+
print("\(LOG_TAG) Applied edit menu filter: \(actions.map { $0.rawValue })")
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private func swizzleCanPerformAction(on view: UIView) {
|
|
195
|
+
let originalClass: AnyClass = type(of: view)
|
|
196
|
+
let className = "AixSwizzled_\(NSStringFromClass(originalClass))"
|
|
197
|
+
|
|
198
|
+
if let existingClass = objc_getClass(className) as? AnyClass {
|
|
199
|
+
object_setClass(view, existingClass)
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
guard let subclass = objc_allocateClassPair(originalClass, className, 0) else { return }
|
|
204
|
+
|
|
205
|
+
let swizzledCanPerform: @convention(block) (AnyObject, Selector, Any?) -> Bool = { obj, action, sender in
|
|
206
|
+
guard let allowed = objc_getAssociatedObject(obj, &allowedActionsKey) as? [Selector] else {
|
|
207
|
+
struct Holder { static let sel = #selector(UIResponder.canPerformAction(_:withSender:)) }
|
|
208
|
+
let superImp = class_getMethodImplementation(class_getSuperclass(type(of: obj)), Holder.sel)
|
|
209
|
+
let superFn = unsafeBitCast(superImp, to: (@convention(c) (AnyObject, Selector, Selector, Any?) -> Bool).self)
|
|
210
|
+
return superFn(obj, Holder.sel, action, sender)
|
|
211
|
+
}
|
|
212
|
+
return allowed.contains(action)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let imp = imp_implementationWithBlock(swizzledCanPerform)
|
|
216
|
+
let sel = #selector(UIResponder.canPerformAction(_:withSender:))
|
|
217
|
+
let method = class_getInstanceMethod(originalClass, sel)!
|
|
218
|
+
let typeEncoding = method_getTypeEncoding(method)!
|
|
219
|
+
class_addMethod(subclass, sel, imp, typeEncoding)
|
|
220
|
+
|
|
221
|
+
objc_registerClassPair(subclass)
|
|
222
|
+
object_setClass(view, subclass)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// MARK: - Helpers
|
|
226
|
+
func fetchImage(from url: URL) async -> UIImage? {
|
|
227
|
+
var req = URLRequest(url: url)
|
|
228
|
+
req.timeoutInterval = 20
|
|
229
|
+
do {
|
|
230
|
+
let (data, resp) = try await URLSession.shared.data(for: req)
|
|
231
|
+
guard let http = resp as? HTTPURLResponse, (200..<300).contains(http.statusCode) else { return nil }
|
|
232
|
+
return UIImage(data: data)
|
|
233
|
+
} catch {
|
|
234
|
+
return nil
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
func checkForFiles(itemProviders: [NSItemProvider], completion: @escaping ([AixInputWrapperOnPasteEvent]) -> Void) {
|
|
239
|
+
var events: [AixInputWrapperOnPasteEvent] = []
|
|
240
|
+
let lock = NSLock()
|
|
241
|
+
let group = DispatchGroup()
|
|
242
|
+
|
|
243
|
+
for provider in itemProviders {
|
|
244
|
+
if provider.hasItemConformingToTypeIdentifier(UTType.pdf.identifier) {
|
|
245
|
+
group.enter()
|
|
246
|
+
provider.loadDataRepresentation(forTypeIdentifier: UTType.pdf.identifier) { data, _ in
|
|
247
|
+
defer { group.leave() }
|
|
248
|
+
guard let data else { return }
|
|
249
|
+
if let uri = try? PasteFileManager.save(data: data, fileExtension: "pdf") {
|
|
250
|
+
print("\(LOG_TAG) PDF saved to \(uri)")
|
|
251
|
+
let fileURL = URL(fileURLWithPath: uri)
|
|
252
|
+
lock.lock()
|
|
253
|
+
defer { lock.unlock() }
|
|
254
|
+
events.append(AixInputWrapperOnPasteEvent(
|
|
255
|
+
type: "file",
|
|
256
|
+
filePath: uri,
|
|
257
|
+
fileExtension: "pdf",
|
|
258
|
+
fileName: fileURL.lastPathComponent
|
|
259
|
+
))
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
} else if provider.hasItemConformingToTypeIdentifier(UTType.svg.identifier) {
|
|
263
|
+
group.enter()
|
|
264
|
+
provider.loadDataRepresentation(forTypeIdentifier: UTType.svg.identifier) { data, _ in
|
|
265
|
+
defer { group.leave() }
|
|
266
|
+
guard let data else { return }
|
|
267
|
+
if let uri = try? PasteFileManager.save(data: data, fileExtension: "svg") {
|
|
268
|
+
print("\(LOG_TAG) SVG saved to \(uri)")
|
|
269
|
+
let fileURL = URL(fileURLWithPath: uri)
|
|
270
|
+
lock.lock()
|
|
271
|
+
defer { lock.unlock() }
|
|
272
|
+
events.append(AixInputWrapperOnPasteEvent(
|
|
273
|
+
type: "file",
|
|
274
|
+
filePath: uri,
|
|
275
|
+
fileExtension: "svg",
|
|
276
|
+
fileName: fileURL.lastPathComponent
|
|
277
|
+
))
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
} else if provider.canLoadObject(ofClass: UIImage.self) {
|
|
281
|
+
group.enter()
|
|
282
|
+
provider.loadObject(ofClass: UIImage.self) { object, _ in
|
|
283
|
+
defer { group.leave() }
|
|
284
|
+
guard let image = object as? UIImage else { return }
|
|
285
|
+
if let uri = try? PasteFileManager.save(image: image) {
|
|
286
|
+
print("\(LOG_TAG) Image saved to \(uri)")
|
|
287
|
+
let fileURL = URL(fileURLWithPath: uri)
|
|
288
|
+
lock.lock()
|
|
289
|
+
defer { lock.unlock() }
|
|
290
|
+
events.append(AixInputWrapperOnPasteEvent(
|
|
291
|
+
type: "image",
|
|
292
|
+
filePath: uri,
|
|
293
|
+
fileExtension: fileURL.pathExtension,
|
|
294
|
+
fileName: fileURL.lastPathComponent
|
|
295
|
+
))
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
group.notify(queue: .main) {
|
|
302
|
+
completion(events)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// MARK: - Paste Handler Delegate
|
|
308
|
+
final class PasteHandlerDelegate: NSObject, UITextPasteDelegate {
|
|
309
|
+
weak var owner: HybridAixInputWrapper?
|
|
310
|
+
|
|
311
|
+
init(owner: HybridAixInputWrapper) {
|
|
312
|
+
self.owner = owner
|
|
313
|
+
super.init()
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
func textPasteConfigurationSupporting(_ textPasteConfigurationSupporting: UITextPasteConfigurationSupporting, transform item: UITextPasteItem) {
|
|
317
|
+
guard let owner else {
|
|
318
|
+
item.setDefaultResult()
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let pb = UIPasteboard.general
|
|
323
|
+
let provider = item.itemProvider
|
|
324
|
+
|
|
325
|
+
// Multiple images on pasteboard
|
|
326
|
+
if let images = pb.images, images.count > 1 {
|
|
327
|
+
print("\(LOG_TAG) Paste delegate: \(images.count) images")
|
|
328
|
+
owner.saveImages(images) { events in
|
|
329
|
+
if !events.isEmpty {
|
|
330
|
+
owner.onPaste?(events)
|
|
331
|
+
}
|
|
332
|
+
item.setNoResult()
|
|
333
|
+
}
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// File types (PDF, SVG)
|
|
338
|
+
if provider.hasItemConformingToTypeIdentifier(UTType.pdf.identifier) ||
|
|
339
|
+
provider.hasItemConformingToTypeIdentifier(UTType.svg.identifier) {
|
|
340
|
+
owner.checkForFiles(itemProviders: [provider]) { events in
|
|
341
|
+
if !events.isEmpty {
|
|
342
|
+
owner.onPaste?(events)
|
|
343
|
+
item.setNoResult()
|
|
344
|
+
} else {
|
|
345
|
+
item.setDefaultResult()
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Single image
|
|
352
|
+
if let image = pb.image {
|
|
353
|
+
owner.saveImages([image]) { events in
|
|
354
|
+
if !events.isEmpty {
|
|
355
|
+
owner.onPaste?(events)
|
|
356
|
+
}
|
|
357
|
+
item.setNoResult()
|
|
358
|
+
}
|
|
359
|
+
return
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// URL (try to fetch image)
|
|
363
|
+
if let url = pb.url {
|
|
364
|
+
Task {
|
|
365
|
+
if let image = await owner.fetchImage(from: url) {
|
|
366
|
+
owner.saveImages([image]) { events in
|
|
367
|
+
if !events.isEmpty {
|
|
368
|
+
owner.onPaste?(events)
|
|
369
|
+
}
|
|
370
|
+
item.setNoResult()
|
|
371
|
+
}
|
|
372
|
+
} else {
|
|
373
|
+
item.setDefaultResult()
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// String URL
|
|
380
|
+
if let string = pb.string, let url = URL(string: string), url.scheme != nil {
|
|
381
|
+
Task {
|
|
382
|
+
if let image = await owner.fetchImage(from: url) {
|
|
383
|
+
owner.saveImages([image]) { events in
|
|
384
|
+
if !events.isEmpty {
|
|
385
|
+
owner.onPaste?(events)
|
|
386
|
+
}
|
|
387
|
+
item.setNoResult()
|
|
388
|
+
}
|
|
389
|
+
} else {
|
|
390
|
+
item.setDefaultResult()
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Large text
|
|
397
|
+
if let text = pb.string {
|
|
398
|
+
let lineCount = text.split(separator: "\n", omittingEmptySubsequences: false).count
|
|
399
|
+
let charCount = text.count
|
|
400
|
+
let maxCharsPropInt = owner.maxChars.map { Int($0) }
|
|
401
|
+
let maxLinesPropInt = owner.maxLines.map { Int($0) }
|
|
402
|
+
if let maxCharsVal = maxCharsPropInt, let maxLinesVal = maxLinesPropInt,
|
|
403
|
+
lineCount > maxLinesVal || charCount > maxCharsVal {
|
|
404
|
+
if let uri = try? PasteFileManager.save(text: text) {
|
|
405
|
+
let fileURL = URL(fileURLWithPath: uri)
|
|
406
|
+
owner.onPaste?([AixInputWrapperOnPasteEvent(
|
|
407
|
+
type: "text",
|
|
408
|
+
filePath: uri,
|
|
409
|
+
fileExtension: "txt",
|
|
410
|
+
fileName: fileURL.lastPathComponent
|
|
411
|
+
)])
|
|
412
|
+
item.setNoResult()
|
|
413
|
+
return
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
item.setDefaultResult()
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// MARK: - Image Drop Delegate
|
|
423
|
+
final class ImageDropDelegate: NSObject, UIDropInteractionDelegate {
|
|
424
|
+
var onImage: ((UIImage) -> Void)?
|
|
425
|
+
|
|
426
|
+
func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool {
|
|
427
|
+
return session.items.contains { $0.itemProvider.canLoadObject(ofClass: UIImage.self) }
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal {
|
|
431
|
+
return UIDropProposal(operation: .copy)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) {
|
|
435
|
+
for item in session.items {
|
|
436
|
+
let provider = item.itemProvider
|
|
437
|
+
if provider.canLoadObject(ofClass: UIImage.self) {
|
|
438
|
+
provider.loadObject(ofClass: UIImage.self) { [weak self] object, _ in
|
|
439
|
+
guard let image = object as? UIImage else { return }
|
|
440
|
+
DispatchQueue.main.async {
|
|
441
|
+
self?.onImage?(image)
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
final class WeakRef<T> {
|
|
4
|
+
public weak var rawRef: AnyObject?
|
|
5
|
+
|
|
6
|
+
public var ref: T? {
|
|
7
|
+
self.rawRef as? T
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
public init(with ref: T) {
|
|
11
|
+
self.rawRef = ref as AnyObject
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
public init?(with ref: T?) {
|
|
15
|
+
guard let unwrappedRef = ref else { return nil }
|
|
16
|
+
self.rawRef = unwrappedRef as AnyObject
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
enum InputType {
|
|
21
|
+
case textField(WeakRef<UITextField>)
|
|
22
|
+
case textView(WeakRef<UITextView>)
|
|
23
|
+
|
|
24
|
+
var textInput: UITextInput? {
|
|
25
|
+
switch self {
|
|
26
|
+
case let .textField(textField):
|
|
27
|
+
return textField.ref
|
|
28
|
+
case let .textView(textView):
|
|
29
|
+
return textView.ref
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
init(textField: UITextField) {
|
|
34
|
+
self = .textField(.init(with: textField))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
init(textView: UITextView) {
|
|
38
|
+
self = .textView(.init(with: textView))
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
enum PasteFileManager {
|
|
4
|
+
|
|
5
|
+
private static let pasteDir: URL = {
|
|
6
|
+
let tmp = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
|
|
7
|
+
return tmp.appendingPathComponent("paste", isDirectory: true)
|
|
8
|
+
}()
|
|
9
|
+
|
|
10
|
+
/// Maximum age for paste files before cleanup (1 hour).
|
|
11
|
+
private static let maxFileAge: TimeInterval = 3600
|
|
12
|
+
|
|
13
|
+
static func save(image: UIImage) throws -> String {
|
|
14
|
+
let data: Data
|
|
15
|
+
let ext: String
|
|
16
|
+
if let png = image.pngData() {
|
|
17
|
+
data = png
|
|
18
|
+
ext = "png"
|
|
19
|
+
} else if let jpg = image.jpegData(compressionQuality: 0.9) {
|
|
20
|
+
data = jpg
|
|
21
|
+
ext = "jpg"
|
|
22
|
+
} else {
|
|
23
|
+
throw NSError(
|
|
24
|
+
domain: "SaveImageError",
|
|
25
|
+
code: 3,
|
|
26
|
+
userInfo: [NSLocalizedDescriptionKey: "Failed to encode image as PNG or JPEG"]
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return try writeToDir(data: data, fileExtension: ext)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
static func save(text: String) throws -> String {
|
|
34
|
+
guard let data = text.data(using: .utf8) else {
|
|
35
|
+
throw NSError(
|
|
36
|
+
domain: "SaveTextError",
|
|
37
|
+
code: 1,
|
|
38
|
+
userInfo: [NSLocalizedDescriptionKey: "Failed to encode text as UTF-8"]
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return try writeToDir(data: data, fileExtension: "txt")
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
static func save(data: Data, fileExtension: String) throws -> String {
|
|
46
|
+
let sanitizedExt = fileExtension.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
47
|
+
return try writeToDir(data: data, fileExtension: sanitizedExt)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// MARK: - Cleanup
|
|
51
|
+
|
|
52
|
+
/// Removes paste files older than `maxFileAge`.
|
|
53
|
+
static func cleanupOldFiles() {
|
|
54
|
+
let fileManager = FileManager.default
|
|
55
|
+
guard let contents = try? fileManager.contentsOfDirectory(
|
|
56
|
+
at: pasteDir,
|
|
57
|
+
includingPropertiesForKeys: [.creationDateKey],
|
|
58
|
+
options: .skipsHiddenFiles
|
|
59
|
+
) else { return }
|
|
60
|
+
|
|
61
|
+
let cutoff = Date().addingTimeInterval(-maxFileAge)
|
|
62
|
+
var removed = 0
|
|
63
|
+
|
|
64
|
+
for fileURL in contents {
|
|
65
|
+
guard let attrs = try? fileURL.resourceValues(forKeys: [.creationDateKey]),
|
|
66
|
+
let created = attrs.creationDate,
|
|
67
|
+
created < cutoff else { continue }
|
|
68
|
+
try? fileManager.removeItem(at: fileURL)
|
|
69
|
+
removed += 1
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if removed > 0 {
|
|
73
|
+
print("[PasteFileManager] Cleaned up \(removed) old paste file(s)")
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// MARK: - Private
|
|
78
|
+
private static func writeToDir(data: Data, fileExtension: String) throws -> String {
|
|
79
|
+
let fileManager = FileManager.default
|
|
80
|
+
|
|
81
|
+
if !fileManager.fileExists(atPath: pasteDir.path) {
|
|
82
|
+
try fileManager.createDirectory(at: pasteDir, withIntermediateDirectories: true, attributes: nil)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let fileName = UUID().uuidString + "." + fileExtension
|
|
86
|
+
let fileURL = pasteDir.appendingPathComponent(fileName)
|
|
87
|
+
|
|
88
|
+
try data.write(to: fileURL, options: .atomic)
|
|
89
|
+
|
|
90
|
+
return fileURL.path
|
|
91
|
+
}
|
|
92
|
+
}
|
package/nitro.json
CHANGED
|
@@ -20,6 +20,14 @@
|
|
|
20
20
|
"AixComposer": {
|
|
21
21
|
"swift": "HybridAixComposer",
|
|
22
22
|
"kotlin": "HybridAixComposer"
|
|
23
|
+
},
|
|
24
|
+
"AixInputWrapper": {
|
|
25
|
+
"swift": "HybridAixInputWrapper",
|
|
26
|
+
"kotlin": "HybridAixInputWrapper"
|
|
27
|
+
},
|
|
28
|
+
"AixDropzone": {
|
|
29
|
+
"swift": "HybridAixDropzone",
|
|
30
|
+
"kotlin": "HybridAixDropzone"
|
|
23
31
|
}
|
|
24
32
|
},
|
|
25
33
|
"ignorePaths": ["**/node_modules"]
|
|
@@ -39,6 +39,10 @@ target_sources(
|
|
|
39
39
|
../nitrogen/generated/shared/c++/views/HybridAixCellViewComponent.cpp
|
|
40
40
|
../nitrogen/generated/shared/c++/HybridAixComposerSpec.cpp
|
|
41
41
|
../nitrogen/generated/shared/c++/views/HybridAixComposerComponent.cpp
|
|
42
|
+
../nitrogen/generated/shared/c++/HybridAixInputWrapperSpec.cpp
|
|
43
|
+
../nitrogen/generated/shared/c++/views/HybridAixInputWrapperComponent.cpp
|
|
44
|
+
../nitrogen/generated/shared/c++/HybridAixDropzoneSpec.cpp
|
|
45
|
+
../nitrogen/generated/shared/c++/views/HybridAixDropzoneComponent.cpp
|
|
42
46
|
# Android-specific Nitrogen C++ sources
|
|
43
47
|
../nitrogen/generated/android/c++/JHybridAixSpec.cpp
|
|
44
48
|
../nitrogen/generated/android/c++/views/JHybridAixStateUpdater.cpp
|
|
@@ -46,6 +50,10 @@ target_sources(
|
|
|
46
50
|
../nitrogen/generated/android/c++/views/JHybridAixCellViewStateUpdater.cpp
|
|
47
51
|
../nitrogen/generated/android/c++/JHybridAixComposerSpec.cpp
|
|
48
52
|
../nitrogen/generated/android/c++/views/JHybridAixComposerStateUpdater.cpp
|
|
53
|
+
../nitrogen/generated/android/c++/JHybridAixInputWrapperSpec.cpp
|
|
54
|
+
../nitrogen/generated/android/c++/views/JHybridAixInputWrapperStateUpdater.cpp
|
|
55
|
+
../nitrogen/generated/android/c++/JHybridAixDropzoneSpec.cpp
|
|
56
|
+
../nitrogen/generated/android/c++/views/JHybridAixDropzoneStateUpdater.cpp
|
|
49
57
|
)
|
|
50
58
|
|
|
51
59
|
# From node_modules/react-native/ReactAndroid/cmake-utils/folly-flags.cmake
|
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
#include "views/JHybridAixCellViewStateUpdater.hpp"
|
|
24
24
|
#include "JHybridAixComposerSpec.hpp"
|
|
25
25
|
#include "views/JHybridAixComposerStateUpdater.hpp"
|
|
26
|
+
#include "JHybridAixInputWrapperSpec.hpp"
|
|
27
|
+
#include "JFunc_void_std__vector_AixInputWrapperOnPasteEvent_.hpp"
|
|
28
|
+
#include "views/JHybridAixInputWrapperStateUpdater.hpp"
|
|
29
|
+
#include "JHybridAixDropzoneSpec.hpp"
|
|
30
|
+
#include "views/JHybridAixDropzoneStateUpdater.hpp"
|
|
26
31
|
#include <NitroModules/DefaultConstructableObject.hpp>
|
|
27
32
|
|
|
28
33
|
namespace margelo::nitro::aix {
|
|
@@ -42,6 +47,11 @@ int initialize(JavaVM* vm) {
|
|
|
42
47
|
margelo::nitro::aix::views::JHybridAixCellViewStateUpdater::registerNatives();
|
|
43
48
|
margelo::nitro::aix::JHybridAixComposerSpec::registerNatives();
|
|
44
49
|
margelo::nitro::aix::views::JHybridAixComposerStateUpdater::registerNatives();
|
|
50
|
+
margelo::nitro::aix::JHybridAixInputWrapperSpec::registerNatives();
|
|
51
|
+
margelo::nitro::aix::JFunc_void_std__vector_AixInputWrapperOnPasteEvent__cxx::registerNatives();
|
|
52
|
+
margelo::nitro::aix::views::JHybridAixInputWrapperStateUpdater::registerNatives();
|
|
53
|
+
margelo::nitro::aix::JHybridAixDropzoneSpec::registerNatives();
|
|
54
|
+
margelo::nitro::aix::views::JHybridAixDropzoneStateUpdater::registerNatives();
|
|
45
55
|
|
|
46
56
|
// Register Nitro Hybrid Objects
|
|
47
57
|
HybridObjectRegistry::registerHybridObjectConstructor(
|
|
@@ -68,6 +78,22 @@ int initialize(JavaVM* vm) {
|
|
|
68
78
|
return instance->cthis()->shared();
|
|
69
79
|
}
|
|
70
80
|
);
|
|
81
|
+
HybridObjectRegistry::registerHybridObjectConstructor(
|
|
82
|
+
"AixInputWrapper",
|
|
83
|
+
[]() -> std::shared_ptr<HybridObject> {
|
|
84
|
+
static DefaultConstructableObject<JHybridAixInputWrapperSpec::javaobject> object("com/aix/HybridAixInputWrapper");
|
|
85
|
+
auto instance = object.create();
|
|
86
|
+
return instance->cthis()->shared();
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
HybridObjectRegistry::registerHybridObjectConstructor(
|
|
90
|
+
"AixDropzone",
|
|
91
|
+
[]() -> std::shared_ptr<HybridObject> {
|
|
92
|
+
static DefaultConstructableObject<JHybridAixDropzoneSpec::javaobject> object("com/aix/HybridAixDropzone");
|
|
93
|
+
auto instance = object.create();
|
|
94
|
+
return instance->cthis()->shared();
|
|
95
|
+
}
|
|
96
|
+
);
|
|
71
97
|
});
|
|
72
98
|
}
|
|
73
99
|
|