@thelacanians/vue-native-cli 0.4.14 → 0.6.0

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 (123) hide show
  1. package/dist/cli.js +789 -200
  2. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Bridge/NativeBridge.kt +156 -5
  3. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VListFactory.kt +33 -13
  4. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VScrollViewFactory.kt +27 -6
  5. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VSliderFactory.kt +9 -2
  6. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VViewFactory.kt +178 -1
  7. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Helpers/EventThrottle.kt +57 -0
  8. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/GeneratedModuleRegistry.kt +28 -0
  9. package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/NativeModuleRegistry.kt +3 -0
  10. package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/ComponentFactoryTest.kt +674 -0
  11. package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/ErrorOverlayViewTest.kt +183 -0
  12. package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/EventThrottleTest.kt +203 -0
  13. package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/HotReloadManagerTest.kt +162 -0
  14. package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/JSPolyfillsTest.kt +153 -0
  15. package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/NativeBridgeTest.kt +6 -3
  16. package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/NativeModuleTest.kt +475 -0
  17. package/native/android/gradle.properties +1 -0
  18. package/native/android/gradlew +1 -1
  19. package/native/ios/VueNativeCore/Package.swift +1 -1
  20. package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/EventThrottle.swift +80 -0
  21. package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/NativeBridge.swift +244 -112
  22. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VListFactory.swift +19 -2
  23. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VScrollViewFactory.swift +9 -4
  24. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VSliderFactory.swift +8 -3
  25. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VTextFactory.swift +43 -0
  26. package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VViewFactory.swift +116 -4
  27. package/native/ios/VueNativeCore/Sources/VueNativeCore/Helpers/GestureWrapper.swift +100 -0
  28. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/GeneratedModuleRegistry.swift +28 -0
  29. package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/NativeModuleRegistry.swift +3 -0
  30. package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/CertificatePinningTests.swift +190 -0
  31. package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/ComponentFactoryTests.swift +585 -0
  32. package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/EventThrottleTests.swift +161 -0
  33. package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/HotReloadManagerTests.swift +88 -0
  34. package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/JSPolyfillsTests.swift +319 -0
  35. package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/NativeModuleTests.swift +400 -0
  36. package/native/macos/VueNativeMacOS/Package.swift +34 -0
  37. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/ErrorOverlayView.swift +112 -0
  38. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/EventThrottle.swift +58 -0
  39. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/HotReloadManager.swift +153 -0
  40. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/JSPolyfills.swift +696 -0
  41. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/JSRuntime.swift +347 -0
  42. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/NativeBridge.swift +877 -0
  43. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/VueNativeWindowController.swift +125 -0
  44. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/ComponentRegistry.swift +209 -0
  45. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VActionSheetFactory.swift +155 -0
  46. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VActivityIndicatorFactory.swift +85 -0
  47. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VAlertDialogFactory.swift +132 -0
  48. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VButtonFactory.swift +83 -0
  49. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VCheckboxFactory.swift +108 -0
  50. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VDropdownFactory.swift +155 -0
  51. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VImageFactory.swift +270 -0
  52. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VInputFactory.swift +257 -0
  53. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VKeyboardAvoidingFactory.swift +22 -0
  54. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VListFactory.swift +324 -0
  55. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VModalFactory.swift +231 -0
  56. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VOutlineViewFactory.swift +276 -0
  57. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VPickerFactory.swift +134 -0
  58. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VPressableFactory.swift +120 -0
  59. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VProgressBarFactory.swift +71 -0
  60. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VRadioFactory.swift +193 -0
  61. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VRefreshControlFactory.swift +25 -0
  62. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSafeAreaFactory.swift +46 -0
  63. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VScrollViewFactory.swift +190 -0
  64. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSectionListFactory.swift +374 -0
  65. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSegmentedControlFactory.swift +125 -0
  66. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSliderFactory.swift +131 -0
  67. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSplitViewFactory.swift +215 -0
  68. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VStatusBarFactory.swift +25 -0
  69. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSwitchFactory.swift +92 -0
  70. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VTextFactory.swift +336 -0
  71. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VToolbarFactory.swift +212 -0
  72. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VVideoFactory.swift +245 -0
  73. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VViewFactory.swift +314 -0
  74. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VWebViewFactory.swift +162 -0
  75. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/NativeComponentFactory.swift +54 -0
  76. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Helpers/ClickableView.swift +100 -0
  77. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Helpers/Extensions.swift +23 -0
  78. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Helpers/GestureWrapper.swift +183 -0
  79. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Helpers/NSColor+Hex.swift +78 -0
  80. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Layout/FlippedView.swift +19 -0
  81. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Layout/LayoutNode.swift +493 -0
  82. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/AnimationModule.swift +354 -0
  83. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/AppStateModule.swift +62 -0
  84. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/BiometryModule.swift +60 -0
  85. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/CameraModule.swift +167 -0
  86. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/ClipboardModule.swift +34 -0
  87. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/DeviceInfoModule.swift +49 -0
  88. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/DragDropModule.swift +50 -0
  89. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/FileDialogModule.swift +86 -0
  90. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/HapticsModule.swift +42 -0
  91. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/KeyboardModule.swift +28 -0
  92. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/LinkingModule.swift +49 -0
  93. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/MenuModule.swift +95 -0
  94. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/NativeModuleRegistry.swift +63 -0
  95. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/NotificationsModule.swift +112 -0
  96. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/PermissionsModule.swift +149 -0
  97. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/ShareModule.swift +37 -0
  98. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/WindowModule.swift +71 -0
  99. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Resources/vue-native-placeholder.js +2 -0
  100. package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Styling/StyleEngine.swift +885 -0
  101. package/native/macos/VueNativeMacOS/Tests/VueNativeMacOSTests/ComponentFactoryTests.swift +80 -0
  102. package/native/macos/VueNativeMacOS/Tests/VueNativeMacOSTests/VueNativeMacOSTests.swift +149 -0
  103. package/native/shared/VueNativeShared/AGENTS.md +129 -0
  104. package/native/shared/VueNativeShared/Package.swift +14 -0
  105. package/native/shared/VueNativeShared/Sources/VueNativeShared/CertificatePinning.swift +134 -0
  106. package/native/shared/VueNativeShared/Sources/VueNativeShared/EventThrottle.swift +78 -0
  107. package/native/shared/VueNativeShared/Sources/VueNativeShared/HotReloadManager.swift +162 -0
  108. package/native/shared/VueNativeShared/Sources/VueNativeShared/JSRuntime.swift +412 -0
  109. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/AsyncStorageModule.swift +68 -0
  110. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/AudioModule.swift +359 -0
  111. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/DatabaseModule.swift +259 -0
  112. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/FileSystemModule.swift +233 -0
  113. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/GeolocationModule.swift +156 -0
  114. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/NetworkModule.swift +59 -0
  115. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/PerformanceModule.swift +113 -0
  116. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/SecureStorageModule.swift +119 -0
  117. package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/WebSocketModule.swift +212 -0
  118. package/native/shared/VueNativeShared/Sources/VueNativeShared/NativeEventDispatcher.swift +6 -0
  119. package/native/shared/VueNativeShared/Sources/VueNativeShared/NativeModule.swift +26 -0
  120. package/native/shared/VueNativeShared/Sources/VueNativeShared/NativeModuleRegistry.swift +37 -0
  121. package/native/shared/VueNativeShared/Sources/VueNativeShared/SharedJSPolyfills.swift +673 -0
  122. package/native/shared/VueNativeShared/Tests/VueNativeSharedTests/VueNativeSharedTests.swift +44 -0
  123. package/package.json +8 -2
@@ -0,0 +1,212 @@
1
+ import AppKit
2
+ import ObjectiveC
3
+
4
+ /// Factory for VToolbar — macOS-specific toolbar component.
5
+ /// Creates a placeholder FlippedView. When the view is added to a window,
6
+ /// an NSToolbar is created and attached to the window.
7
+ ///
8
+ /// Props:
9
+ /// - items: [{ id, label, icon?, action? }]
10
+ /// - displayMode: "iconOnly" | "labelOnly" | "iconAndLabel"
11
+ /// - showsBaselineSeparator: Bool
12
+ ///
13
+ /// Events:
14
+ /// - itemClick -> { id }
15
+ final class VToolbarFactory: NativeComponentFactory {
16
+
17
+ // MARK: - Associated object keys
18
+
19
+ private static var toolbarDelegateKey: UInt8 = 0
20
+ private static var toolbarItemsKey: UInt8 = 0
21
+ private static var toolbarKey: UInt8 = 0
22
+ private static var itemClickHandlerKey: UInt8 = 0
23
+
24
+ // MARK: - NativeComponentFactory
25
+
26
+ func createView() -> NSView {
27
+ let view = FlippedView()
28
+ view.ensureLayoutNode()
29
+ return view
30
+ }
31
+
32
+ func updateProp(view: NSView, key: String, value: Any?) {
33
+ switch key {
34
+ case "items":
35
+ guard let items = value as? [[String: Any]] else { return }
36
+ objc_setAssociatedObject(
37
+ view, &VToolbarFactory.toolbarItemsKey,
38
+ items, .OBJC_ASSOCIATION_RETAIN_NONATOMIC
39
+ )
40
+ rebuildToolbar(for: view)
41
+
42
+ case "displayMode":
43
+ guard let toolbar = objc_getAssociatedObject(
44
+ view, &VToolbarFactory.toolbarKey
45
+ ) as? NSToolbar else { return }
46
+
47
+ if let mode = value as? String {
48
+ switch mode {
49
+ case "iconOnly":
50
+ toolbar.displayMode = .iconOnly
51
+ case "labelOnly":
52
+ toolbar.displayMode = .labelOnly
53
+ case "iconAndLabel":
54
+ toolbar.displayMode = .iconAndLabel
55
+ default:
56
+ toolbar.displayMode = .default
57
+ }
58
+ }
59
+
60
+ case "showsBaselineSeparator":
61
+ guard let toolbar = objc_getAssociatedObject(
62
+ view, &VToolbarFactory.toolbarKey
63
+ ) as? NSToolbar else { return }
64
+ toolbar.showsBaselineSeparator = (value as? Bool) ?? true
65
+
66
+ default:
67
+ StyleEngine.apply(key: key, value: value, to: view)
68
+ }
69
+ }
70
+
71
+ func addEventListener(view: NSView, event: String, handler: @escaping (Any?) -> Void) {
72
+ switch event {
73
+ case "itemClick":
74
+ objc_setAssociatedObject(
75
+ view, &VToolbarFactory.itemClickHandlerKey,
76
+ handler as AnyObject, .OBJC_ASSOCIATION_RETAIN_NONATOMIC
77
+ )
78
+
79
+ default:
80
+ break
81
+ }
82
+ }
83
+
84
+ func removeEventListener(view: NSView, event: String) {
85
+ switch event {
86
+ case "itemClick":
87
+ objc_setAssociatedObject(
88
+ view, &VToolbarFactory.itemClickHandlerKey,
89
+ nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC
90
+ )
91
+
92
+ default:
93
+ break
94
+ }
95
+ }
96
+
97
+ // MARK: - Toolbar management
98
+
99
+ private func rebuildToolbar(for view: NSView) {
100
+ guard let window = view.window else {
101
+ // View not yet in a window — defer until it appears.
102
+ // We'll use viewDidMoveToWindow in the observation.
103
+ setupWindowObserver(for: view)
104
+ return
105
+ }
106
+
107
+ let items = objc_getAssociatedObject(
108
+ view, &VToolbarFactory.toolbarItemsKey
109
+ ) as? [[String: Any]] ?? []
110
+
111
+ let itemClickHandler: ((Any?) -> Void)? = {
112
+ let stored = objc_getAssociatedObject(
113
+ view, &VToolbarFactory.itemClickHandlerKey
114
+ )
115
+ return stored as? (Any?) -> Void
116
+ }()
117
+
118
+ let delegate = ToolbarDelegate(items: items, onItemClick: itemClickHandler)
119
+ objc_setAssociatedObject(
120
+ view, &VToolbarFactory.toolbarDelegateKey,
121
+ delegate, .OBJC_ASSOCIATION_RETAIN_NONATOMIC
122
+ )
123
+
124
+ let toolbar = NSToolbar(identifier: "VueNativeToolbar-\(ObjectIdentifier(view).hashValue)")
125
+ toolbar.delegate = delegate
126
+ toolbar.displayMode = .default
127
+ toolbar.showsBaselineSeparator = true
128
+
129
+ objc_setAssociatedObject(
130
+ view, &VToolbarFactory.toolbarKey,
131
+ toolbar, .OBJC_ASSOCIATION_RETAIN_NONATOMIC
132
+ )
133
+
134
+ window.toolbar = toolbar
135
+ }
136
+
137
+ private func setupWindowObserver(for view: NSView) {
138
+ // Observe when the view moves to a window
139
+ let observer = NotificationCenter.default.addObserver(
140
+ forName: NSView.frameDidChangeNotification,
141
+ object: nil,
142
+ queue: .main
143
+ ) { [weak self, weak view] _ in
144
+ guard let self = self, let view = view, view.window != nil else { return }
145
+ self.rebuildToolbar(for: view)
146
+ }
147
+ // Store observer to keep it alive; will be replaced on next call
148
+ objc_setAssociatedObject(
149
+ view, &VToolbarFactory.toolbarDelegateKey,
150
+ observer, .OBJC_ASSOCIATION_RETAIN_NONATOMIC
151
+ )
152
+ }
153
+ }
154
+
155
+ // MARK: - ToolbarDelegate
156
+
157
+ private final class ToolbarDelegate: NSObject, NSToolbarDelegate {
158
+
159
+ private let items: [[String: Any]]
160
+ private let onItemClick: ((Any?) -> Void)?
161
+
162
+ init(items: [[String: Any]], onItemClick: ((Any?) -> Void)?) {
163
+ self.items = items
164
+ self.onItemClick = onItemClick
165
+ super.init()
166
+ }
167
+
168
+ func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
169
+ return items.map { item in
170
+ NSToolbarItem.Identifier(item["id"] as? String ?? UUID().uuidString)
171
+ } + [.flexibleSpace, .space]
172
+ }
173
+
174
+ func toolbarDefaultItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
175
+ return items.map { item in
176
+ NSToolbarItem.Identifier(item["id"] as? String ?? UUID().uuidString)
177
+ }
178
+ }
179
+
180
+ func toolbar(
181
+ _ toolbar: NSToolbar,
182
+ itemForItemIdentifier itemIdentifier: NSToolbarItem.Identifier,
183
+ willBeInsertedIntoToolbar flag: Bool
184
+ ) -> NSToolbarItem? {
185
+ guard let itemData = items.first(where: {
186
+ ($0["id"] as? String) == itemIdentifier.rawValue
187
+ }) else {
188
+ return nil
189
+ }
190
+
191
+ let toolbarItem = NSToolbarItem(itemIdentifier: itemIdentifier)
192
+ toolbarItem.label = itemData["label"] as? String ?? ""
193
+ toolbarItem.toolTip = itemData["label"] as? String
194
+
195
+ if let iconName = itemData["icon"] as? String {
196
+ if let systemImage = NSImage(systemSymbolName: iconName, accessibilityDescription: nil) {
197
+ toolbarItem.image = systemImage
198
+ } else {
199
+ toolbarItem.image = NSImage(named: iconName)
200
+ }
201
+ }
202
+
203
+ toolbarItem.target = self
204
+ toolbarItem.action = #selector(toolbarItemClicked(_:))
205
+
206
+ return toolbarItem
207
+ }
208
+
209
+ @objc private func toolbarItemClicked(_ sender: NSToolbarItem) {
210
+ onItemClick?(["id": sender.itemIdentifier.rawValue])
211
+ }
212
+ }
@@ -0,0 +1,245 @@
1
+ import AppKit
2
+ import AVFoundation
3
+ import AVKit
4
+ import ObjectiveC
5
+
6
+ // File-level keys used in non-isolated contexts (e.g. deinit).
7
+ private nonisolated(unsafe) var _timeObserverKey: UInt8 = 7
8
+ private nonisolated(unsafe) var _endObserverKey: UInt8 = 9
9
+
10
+ /// Factory for VVideo — video playback via AVPlayer + AVPlayerLayer on macOS.
11
+ final class VVideoFactory: NativeComponentFactory {
12
+
13
+ // MARK: - Associated object keys
14
+
15
+ private static var onReadyKey: UInt8 = 0
16
+ private static var onPlayKey: UInt8 = 1
17
+ private static var onPauseKey: UInt8 = 2
18
+ private static var onEndKey: UInt8 = 3
19
+ private static var onErrorKey: UInt8 = 4
20
+ private static var onProgressKey: UInt8 = 5
21
+ private static var playerKey: UInt8 = 6
22
+ private static var statusObserverKey: UInt8 = 8
23
+
24
+ // MARK: - NativeComponentFactory
25
+
26
+ func createView() -> NSView {
27
+ let view = VideoContainerView()
28
+ view.wantsLayer = true
29
+ view.ensureLayoutNode()
30
+ return view
31
+ }
32
+
33
+ func updateProp(view: NSView, key: String, value: Any?) {
34
+ guard let container = view as? VideoContainerView else {
35
+ StyleEngine.apply(key: key, value: value, to: view)
36
+ return
37
+ }
38
+
39
+ switch key {
40
+ case "source":
41
+ if let sourceDict = value as? [String: Any],
42
+ let uriString = sourceDict["uri"] as? String,
43
+ let url = URL(string: uriString) {
44
+ setupPlayer(url: url, in: container)
45
+ } else {
46
+ cleanupPlayer(for: container)
47
+ }
48
+
49
+ case "uri":
50
+ if let uriString = value as? String, let url = URL(string: uriString) {
51
+ setupPlayer(url: url, in: container)
52
+ } else {
53
+ cleanupPlayer(for: container)
54
+ }
55
+
56
+ case "paused":
57
+ let paused = value as? Bool ?? false
58
+ if paused {
59
+ container.player?.pause()
60
+ } else {
61
+ container.player?.play()
62
+ }
63
+
64
+ case "muted":
65
+ container.player?.isMuted = value as? Bool ?? false
66
+
67
+ case "volume":
68
+ if let vol = value as? Double {
69
+ container.player?.volume = Float(max(0, min(1, vol)))
70
+ } else if let vol = value as? Int {
71
+ container.player?.volume = Float(max(0, min(1, Double(vol))))
72
+ }
73
+
74
+ case "repeat", "loop":
75
+ container.loop = value as? Bool ?? false
76
+
77
+ case "resizeMode":
78
+ let gravity: AVLayerVideoGravity
79
+ switch value as? String {
80
+ case "contain": gravity = .resizeAspect
81
+ case "stretch": gravity = .resize
82
+ default: gravity = .resizeAspectFill // "cover"
83
+ }
84
+ container.playerLayer?.videoGravity = gravity
85
+
86
+ case "rate":
87
+ if let rate = value as? Double {
88
+ container.player?.rate = Float(rate)
89
+ } else if let rate = value as? Int {
90
+ container.player?.rate = Float(rate)
91
+ }
92
+
93
+ default:
94
+ StyleEngine.apply(key: key, value: value, to: view)
95
+ }
96
+ }
97
+
98
+ func addEventListener(view: NSView, event: String, handler: @escaping (Any?) -> Void) {
99
+ let keyPtr: UnsafeRawPointer
100
+ switch event {
101
+ case "ready": keyPtr = UnsafeRawPointer(&VVideoFactory.onReadyKey)
102
+ case "play": keyPtr = UnsafeRawPointer(&VVideoFactory.onPlayKey)
103
+ case "pause": keyPtr = UnsafeRawPointer(&VVideoFactory.onPauseKey)
104
+ case "end": keyPtr = UnsafeRawPointer(&VVideoFactory.onEndKey)
105
+ case "error": keyPtr = UnsafeRawPointer(&VVideoFactory.onErrorKey)
106
+ case "progress": keyPtr = UnsafeRawPointer(&VVideoFactory.onProgressKey)
107
+ default: return
108
+ }
109
+ objc_setAssociatedObject(view, keyPtr, handler as AnyObject, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
110
+ }
111
+
112
+ func removeEventListener(view: NSView, event: String) {
113
+ let keyPtr: UnsafeRawPointer
114
+ switch event {
115
+ case "ready": keyPtr = UnsafeRawPointer(&VVideoFactory.onReadyKey)
116
+ case "play": keyPtr = UnsafeRawPointer(&VVideoFactory.onPlayKey)
117
+ case "pause": keyPtr = UnsafeRawPointer(&VVideoFactory.onPauseKey)
118
+ case "end": keyPtr = UnsafeRawPointer(&VVideoFactory.onEndKey)
119
+ case "error": keyPtr = UnsafeRawPointer(&VVideoFactory.onErrorKey)
120
+ case "progress": keyPtr = UnsafeRawPointer(&VVideoFactory.onProgressKey)
121
+ default: return
122
+ }
123
+ objc_setAssociatedObject(view, keyPtr, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
124
+ }
125
+
126
+ // MARK: - Player setup
127
+
128
+ private func setupPlayer(url: URL, in container: VideoContainerView) {
129
+ cleanupPlayer(for: container)
130
+
131
+ let playerItem = AVPlayerItem(url: url)
132
+ let player = AVPlayer(playerItem: playerItem)
133
+ container.player = player
134
+
135
+ let playerLayer = AVPlayerLayer(player: player)
136
+ playerLayer.videoGravity = .resizeAspectFill
137
+ playerLayer.frame = container.bounds
138
+ container.layer?.addSublayer(playerLayer)
139
+ container.playerLayer = playerLayer
140
+
141
+ // Observe item status for ready/error
142
+ let statusObserver = playerItem.observe(\.status, options: [.new]) { [weak container] item, _ in
143
+ DispatchQueue.main.async {
144
+ guard let container = container else { return }
145
+ switch item.status {
146
+ case .readyToPlay:
147
+ let duration = CMTimeGetSeconds(item.duration)
148
+ self.fireEvent(for: container, key: &VVideoFactory.onReadyKey,
149
+ payload: ["duration": duration.isFinite ? duration : 0])
150
+ case .failed:
151
+ let message = item.error?.localizedDescription ?? "Unknown playback error"
152
+ self.fireEvent(for: container, key: &VVideoFactory.onErrorKey,
153
+ payload: ["message": message])
154
+ default:
155
+ break
156
+ }
157
+ }
158
+ }
159
+ objc_setAssociatedObject(container, &VVideoFactory.statusObserverKey, statusObserver, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
160
+
161
+ // Periodic time observer for progress
162
+ let interval = CMTime(seconds: 0.25, preferredTimescale: CMTimeScale(NSEC_PER_SEC))
163
+ let timeObserver = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak container] time in
164
+ guard let container = container,
165
+ let duration = container.player?.currentItem?.duration else { return }
166
+ let currentTime = CMTimeGetSeconds(time)
167
+ let dur = CMTimeGetSeconds(duration)
168
+ if currentTime.isFinite && dur.isFinite {
169
+ self.fireEvent(for: container, key: &VVideoFactory.onProgressKey,
170
+ payload: ["currentTime": currentTime, "duration": dur])
171
+ }
172
+ }
173
+ objc_setAssociatedObject(container, &_timeObserverKey, timeObserver as AnyObject, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
174
+
175
+ // End-of-playback observer
176
+ let endObserver = NotificationCenter.default.addObserver(
177
+ forName: .AVPlayerItemDidPlayToEndTime,
178
+ object: playerItem,
179
+ queue: .main
180
+ ) { [weak container] _ in
181
+ guard let container = container else { return }
182
+ self.fireEvent(for: container, key: &VVideoFactory.onEndKey, payload: nil)
183
+ if container.loop {
184
+ container.player?.seek(to: .zero)
185
+ container.player?.play()
186
+ }
187
+ }
188
+ objc_setAssociatedObject(container, &_endObserverKey, endObserver as AnyObject, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
189
+ }
190
+
191
+ private func cleanupPlayer(for container: VideoContainerView) {
192
+ // Remove time observer
193
+ if let timeObserver = objc_getAssociatedObject(container, &_timeObserverKey) {
194
+ container.player?.removeTimeObserver(timeObserver)
195
+ objc_setAssociatedObject(container, &_timeObserverKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
196
+ }
197
+
198
+ // Remove end observer
199
+ if let endObserver = objc_getAssociatedObject(container, &_endObserverKey) {
200
+ NotificationCenter.default.removeObserver(endObserver)
201
+ objc_setAssociatedObject(container, &_endObserverKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
202
+ }
203
+
204
+ // Remove status observer
205
+ objc_setAssociatedObject(container, &VVideoFactory.statusObserverKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
206
+
207
+ container.player?.pause()
208
+ container.player = nil
209
+ container.playerLayer?.removeFromSuperlayer()
210
+ container.playerLayer = nil
211
+ }
212
+
213
+ private func fireEvent(for view: NSView, key: inout UInt8, payload: Any?) {
214
+ if let handler = objc_getAssociatedObject(view, &key) as? ((Any?) -> Void) {
215
+ handler(payload)
216
+ }
217
+ }
218
+ }
219
+
220
+ // MARK: - VideoContainerView
221
+
222
+ /// Custom NSView that holds AVPlayerLayer and resizes it on layout.
223
+ private class VideoContainerView: FlippedView {
224
+ var player: AVPlayer?
225
+ var playerLayer: AVPlayerLayer?
226
+ var loop: Bool = false
227
+
228
+ override func layout() {
229
+ super.layout()
230
+ playerLayer?.frame = bounds
231
+ }
232
+
233
+ deinit {
234
+ // Clean up player resources
235
+ if let timeObserver = objc_getAssociatedObject(self, &_timeObserverKey) {
236
+ player?.removeTimeObserver(timeObserver)
237
+ }
238
+ if let endObserver = objc_getAssociatedObject(self, &_endObserverKey) {
239
+ NotificationCenter.default.removeObserver(endObserver)
240
+ }
241
+ player?.pause()
242
+ player = nil
243
+ playerLayer?.removeFromSuperlayer()
244
+ }
245
+ }