@thelacanians/vue-native-cli 0.4.15 → 0.6.2
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/dist/cli.js +329 -15
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Bridge/NativeBridge.kt +118 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Components/Factories/VViewFactory.kt +178 -1
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/GeneratedModuleRegistry.kt +28 -0
- package/native/android/VueNativeCore/src/main/kotlin/com/vuenative/core/Modules/NativeModuleRegistry.kt +3 -0
- package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/ComponentFactoryTest.kt +674 -0
- package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/ErrorOverlayViewTest.kt +183 -0
- package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/EventThrottleTest.kt +203 -0
- package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/HotReloadManagerTest.kt +162 -0
- package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/JSPolyfillsTest.kt +153 -0
- package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/NativeBridgeTest.kt +6 -3
- package/native/android/VueNativeCore/src/test/kotlin/com/vuenative/core/NativeModuleTest.kt +475 -0
- package/native/android/gradle.properties +1 -0
- package/native/android/gradlew +1 -1
- package/native/ios/VueNativeCore/Package.swift +1 -1
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/EventThrottle.swift +1 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Bridge/NativeBridge.swift +143 -5
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VTextFactory.swift +43 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Components/Factories/VViewFactory.swift +116 -4
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Helpers/GestureWrapper.swift +100 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/GeneratedModuleRegistry.swift +28 -0
- package/native/ios/VueNativeCore/Sources/VueNativeCore/Modules/NativeModuleRegistry.swift +3 -0
- package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/CertificatePinningTests.swift +190 -0
- package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/ComponentFactoryTests.swift +585 -0
- package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/EventThrottleTests.swift +161 -0
- package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/HotReloadManagerTests.swift +88 -0
- package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/JSPolyfillsTests.swift +319 -0
- package/native/ios/VueNativeCore/Tests/VueNativeCoreTests/NativeModuleTests.swift +400 -0
- package/native/macos/VueNativeMacOS/Package.swift +34 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/ErrorOverlayView.swift +112 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/EventThrottle.swift +58 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/HotReloadManager.swift +153 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/JSPolyfills.swift +696 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/JSRuntime.swift +347 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/NativeBridge.swift +877 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Bridge/VueNativeWindowController.swift +125 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/ComponentRegistry.swift +209 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VActionSheetFactory.swift +155 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VActivityIndicatorFactory.swift +85 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VAlertDialogFactory.swift +132 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VButtonFactory.swift +83 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VCheckboxFactory.swift +108 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VDropdownFactory.swift +155 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VImageFactory.swift +270 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VInputFactory.swift +257 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VKeyboardAvoidingFactory.swift +22 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VListFactory.swift +324 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VModalFactory.swift +231 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VOutlineViewFactory.swift +276 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VPickerFactory.swift +134 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VPressableFactory.swift +120 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VProgressBarFactory.swift +71 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VRadioFactory.swift +193 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VRefreshControlFactory.swift +25 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSafeAreaFactory.swift +46 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VScrollViewFactory.swift +190 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSectionListFactory.swift +374 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSegmentedControlFactory.swift +125 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSliderFactory.swift +131 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSplitViewFactory.swift +215 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VStatusBarFactory.swift +25 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VSwitchFactory.swift +92 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VTextFactory.swift +336 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VToolbarFactory.swift +212 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VVideoFactory.swift +245 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VViewFactory.swift +314 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VWebViewFactory.swift +162 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/NativeComponentFactory.swift +54 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Helpers/ClickableView.swift +100 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Helpers/Extensions.swift +23 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Helpers/GestureWrapper.swift +183 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Helpers/NSColor+Hex.swift +78 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Layout/FlippedView.swift +19 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Layout/LayoutNode.swift +493 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/AnimationModule.swift +354 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/AppStateModule.swift +62 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/BiometryModule.swift +60 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/CameraModule.swift +167 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/ClipboardModule.swift +34 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/DeviceInfoModule.swift +49 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/DragDropModule.swift +50 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/FileDialogModule.swift +86 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/HapticsModule.swift +42 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/KeyboardModule.swift +28 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/LinkingModule.swift +49 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/MenuModule.swift +95 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/NativeModuleRegistry.swift +63 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/NotificationsModule.swift +112 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/PermissionsModule.swift +149 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/ShareModule.swift +37 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Modules/WindowModule.swift +71 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Resources/vue-native-placeholder.js +2 -0
- package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Styling/StyleEngine.swift +885 -0
- package/native/macos/VueNativeMacOS/Tests/VueNativeMacOSTests/ComponentFactoryTests.swift +80 -0
- package/native/macos/VueNativeMacOS/Tests/VueNativeMacOSTests/VueNativeMacOSTests.swift +149 -0
- package/native/shared/VueNativeShared/AGENTS.md +129 -0
- package/native/shared/VueNativeShared/Package.swift +14 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/CertificatePinning.swift +134 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/EventThrottle.swift +78 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/HotReloadManager.swift +162 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/JSRuntime.swift +412 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/AsyncStorageModule.swift +68 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/AudioModule.swift +359 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/DatabaseModule.swift +259 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/FileSystemModule.swift +233 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/GeolocationModule.swift +156 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/NetworkModule.swift +59 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/PerformanceModule.swift +113 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/SecureStorageModule.swift +119 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/Modules/WebSocketModule.swift +212 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/NativeEventDispatcher.swift +6 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/NativeModule.swift +26 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/NativeModuleRegistry.swift +37 -0
- package/native/shared/VueNativeShared/Sources/VueNativeShared/SharedJSPolyfills.swift +673 -0
- package/native/shared/VueNativeShared/Tests/VueNativeSharedTests/VueNativeSharedTests.swift +44 -0
- 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
|
+
}
|
package/native/macos/VueNativeMacOS/Sources/VueNativeMacOS/Components/Factories/VVideoFactory.swift
ADDED
|
@@ -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
|
+
}
|