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.
Files changed (77) hide show
  1. package/ios/EditMenuDefaultActions.swift +70 -0
  2. package/ios/HybridAix.swift +28 -3
  3. package/ios/HybridAixCellView.swift +0 -1
  4. package/ios/HybridAixComposer.swift +82 -0
  5. package/ios/HybridAixDropzone.swift +104 -0
  6. package/ios/HybridAixInputWrapper.swift +447 -0
  7. package/ios/InputType.swift +40 -0
  8. package/ios/PasteFileManager.swift +92 -0
  9. package/nitro.json +8 -0
  10. package/nitrogen/generated/android/Aix+autolinking.cmake +8 -0
  11. package/nitrogen/generated/android/AixOnLoad.cpp +26 -0
  12. package/nitrogen/generated/android/c++/JAixInputWrapperOnPasteEvent.hpp +70 -0
  13. package/nitrogen/generated/android/c++/JFunc_void_std__vector_AixInputWrapperOnPasteEvent_.hpp +98 -0
  14. package/nitrogen/generated/android/c++/JHybridAixComposerSpec.cpp +9 -0
  15. package/nitrogen/generated/android/c++/JHybridAixComposerSpec.hpp +2 -0
  16. package/nitrogen/generated/android/c++/JHybridAixDropzoneSpec.cpp +72 -0
  17. package/nitrogen/generated/android/c++/JHybridAixDropzoneSpec.hpp +66 -0
  18. package/nitrogen/generated/android/c++/JHybridAixInputWrapperSpec.cpp +144 -0
  19. package/nitrogen/generated/android/c++/JHybridAixInputWrapperSpec.hpp +74 -0
  20. package/nitrogen/generated/android/c++/views/JHybridAixComposerStateUpdater.cpp +4 -0
  21. package/nitrogen/generated/android/c++/views/JHybridAixDropzoneStateUpdater.cpp +56 -0
  22. package/nitrogen/generated/android/c++/views/JHybridAixDropzoneStateUpdater.hpp +49 -0
  23. package/nitrogen/generated/android/c++/views/JHybridAixInputWrapperStateUpdater.cpp +72 -0
  24. package/nitrogen/generated/android/c++/views/JHybridAixInputWrapperStateUpdater.hpp +49 -0
  25. package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/AixInputWrapperOnPasteEvent.kt +47 -0
  26. package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/Func_void_std__vector_AixInputWrapperOnPasteEvent_.kt +80 -0
  27. package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/HybridAixComposerSpec.kt +6 -0
  28. package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/HybridAixDropzoneSpec.kt +67 -0
  29. package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/HybridAixInputWrapperSpec.kt +91 -0
  30. package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/views/HybridAixDropzoneManager.kt +50 -0
  31. package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/views/HybridAixDropzoneStateUpdater.kt +23 -0
  32. package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/views/HybridAixInputWrapperManager.kt +50 -0
  33. package/nitrogen/generated/android/kotlin/com/margelo/nitro/aix/views/HybridAixInputWrapperStateUpdater.kt +23 -0
  34. package/nitrogen/generated/ios/Aix-Swift-Cxx-Bridge.cpp +42 -0
  35. package/nitrogen/generated/ios/Aix-Swift-Cxx-Bridge.hpp +112 -0
  36. package/nitrogen/generated/ios/Aix-Swift-Cxx-Umbrella.hpp +14 -0
  37. package/nitrogen/generated/ios/AixAutolinking.mm +16 -0
  38. package/nitrogen/generated/ios/AixAutolinking.swift +30 -0
  39. package/nitrogen/generated/ios/c++/HybridAixComposerSpecSwift.hpp +7 -0
  40. package/nitrogen/generated/ios/c++/HybridAixDropzoneSpecSwift.cpp +11 -0
  41. package/nitrogen/generated/ios/c++/HybridAixDropzoneSpecSwift.hpp +80 -0
  42. package/nitrogen/generated/ios/c++/HybridAixInputWrapperSpecSwift.cpp +11 -0
  43. package/nitrogen/generated/ios/c++/HybridAixInputWrapperSpecSwift.hpp +108 -0
  44. package/nitrogen/generated/ios/c++/views/HybridAixComposerComponent.mm +5 -0
  45. package/nitrogen/generated/ios/c++/views/HybridAixDropzoneComponent.mm +96 -0
  46. package/nitrogen/generated/ios/c++/views/HybridAixInputWrapperComponent.mm +116 -0
  47. package/nitrogen/generated/ios/swift/AixInputWrapperOnPasteEvent.swift +107 -0
  48. package/nitrogen/generated/ios/swift/Func_void_std__vector_AixInputWrapperOnPasteEvent_.swift +47 -0
  49. package/nitrogen/generated/ios/swift/HybridAixComposerSpec.swift +1 -0
  50. package/nitrogen/generated/ios/swift/HybridAixComposerSpec_cxx.swift +24 -0
  51. package/nitrogen/generated/ios/swift/HybridAixDropzoneSpec.swift +56 -0
  52. package/nitrogen/generated/ios/swift/HybridAixDropzoneSpec_cxx.swift +167 -0
  53. package/nitrogen/generated/ios/swift/HybridAixInputWrapperSpec.swift +60 -0
  54. package/nitrogen/generated/ios/swift/HybridAixInputWrapperSpec_cxx.swift +261 -0
  55. package/nitrogen/generated/shared/c++/AixInputWrapperOnPasteEvent.hpp +88 -0
  56. package/nitrogen/generated/shared/c++/HybridAixComposerSpec.cpp +2 -0
  57. package/nitrogen/generated/shared/c++/HybridAixComposerSpec.hpp +2 -0
  58. package/nitrogen/generated/shared/c++/HybridAixDropzoneSpec.cpp +22 -0
  59. package/nitrogen/generated/shared/c++/HybridAixDropzoneSpec.hpp +67 -0
  60. package/nitrogen/generated/shared/c++/HybridAixInputWrapperSpec.cpp +30 -0
  61. package/nitrogen/generated/shared/c++/HybridAixInputWrapperSpec.hpp +76 -0
  62. package/nitrogen/generated/shared/c++/views/HybridAixComposerComponent.cpp +12 -0
  63. package/nitrogen/generated/shared/c++/views/HybridAixComposerComponent.hpp +1 -0
  64. package/nitrogen/generated/shared/c++/views/HybridAixDropzoneComponent.cpp +87 -0
  65. package/nitrogen/generated/shared/c++/views/HybridAixDropzoneComponent.hpp +109 -0
  66. package/nitrogen/generated/shared/c++/views/HybridAixInputWrapperComponent.cpp +135 -0
  67. package/nitrogen/generated/shared/c++/views/HybridAixInputWrapperComponent.hpp +114 -0
  68. package/nitrogen/generated/shared/json/AixComposerConfig.json +1 -0
  69. package/nitrogen/generated/shared/json/AixDropzoneConfig.json +10 -0
  70. package/nitrogen/generated/shared/json/AixInputWrapperConfig.json +14 -0
  71. package/package.json +1 -1
  72. package/src/dropzone.ios.tsx +27 -0
  73. package/src/dropzone.tsx +10 -0
  74. package/src/index.ts +3 -0
  75. package/src/input-wrapper.ios.tsx +30 -0
  76. package/src/input-wrapper.tsx +17 -0
  77. 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