catalyst-core 0.1.0-beta.2 → 0.1.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/bin/catalyst.js +8 -1
- package/changelog.md +6 -0
- package/dist/native/androidProject/app/build.gradle.kts +9 -0
- package/dist/native/androidProject/app/src/main/AndroidManifest.xml +2 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/BridgeMessageValidator.kt +1 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +17 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +31 -3
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +148 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeCameraManager.kt +337 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/TransitionManager.kt +202 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt +3 -3
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/BarcodeDetector.kt +123 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/CameraSessionManager.kt +244 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/HoldController.kt +70 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/TorchController.kt +57 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/VideoStreamState.kt +41 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/VideoStreamStateMachine.kt +40 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/ViewfinderMapper.kt +75 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/camera/ZoomController.kt +156 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/CatalystPlugin.kt +7 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/GeneratedPluginIndex.kt +6 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/plugins/PluginBridge.kt +253 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/security/FridaDetector.kt +16 -5
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +42 -5
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/FrameworkServerUtils.kt +7 -3
- package/dist/native/androidProject/app/src/main/res/drawable/debug_viewfinder_border.xml +8 -0
- package/dist/native/androidProject/app/src/main/res/layout/activity_main.xml +37 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/plugins/PluginBridgeTest.kt +139 -0
- package/dist/native/androidSetup.js +1 -1
- package/dist/native/bridge/WebBridge.js +5 -5
- package/dist/native/bridge/constants/NativeInterfaces.js +9 -5
- package/dist/native/bridge/hooks/index.js +1 -0
- package/dist/native/bridge/hooks/useCamera.js +9 -0
- package/dist/native/bridge/hooks/useDataProtection.js +2 -0
- package/dist/native/bridge/hooks/useDeviceInfo.js +2 -0
- package/dist/native/bridge/hooks/useFilePicker.js +2 -0
- package/dist/native/bridge/hooks/useGoogleSignIn.js +1 -0
- package/dist/native/bridge/hooks/useHapticFeedback.js +1 -0
- package/dist/native/bridge/hooks/useIntent.js +1 -0
- package/dist/native/bridge/hooks/useNativeTransition.js +43 -0
- package/dist/native/bridge/hooks/useNetworkStatus.js +3 -0
- package/dist/native/bridge/hooks/useNotification.js +2 -0
- package/dist/native/bridge/hooks/useSafeArea.js +2 -0
- package/dist/native/bridge/hooks/useVideoStream.js +6 -0
- package/dist/native/bridge/hooks/utils.js +1 -0
- package/dist/native/bridge/hooks.js +1 -137
- package/dist/native/bridge/useBaseHook.js +12 -7
- package/dist/native/bridge/utils/NativeBridge.js +41 -6
- package/dist/native/buildAppAndroid.js +7 -9
- package/dist/native/buildAppIos.js +14 -22
- package/dist/native/internalPluginUtils.js +1 -0
- package/dist/native/iosnativeWebView/Package.resolved +36 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/BarcodeDetector.swift +116 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/CameraPreviewView.swift +58 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/CameraSessionManager.swift +308 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/HoldController.swift +63 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/NativeCameraManager.swift +194 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/TorchController.swift +60 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/VideoStreamState.swift +44 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/VideoStreamStateMachine.swift +60 -0
- package/dist/native/iosnativeWebView/Sources/Core/Camera/ZoomController.swift +130 -0
- package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +11 -1
- package/dist/native/iosnativeWebView/Sources/Core/Plugins/CatalystPlugin.swift +5 -0
- package/dist/native/iosnativeWebView/Sources/Core/Plugins/GeneratedPluginIndex.swift +6 -0
- package/dist/native/iosnativeWebView/Sources/Core/Plugins/PluginBridge.swift +364 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +4 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +227 -4
- package/dist/native/iosnativeWebView/Sources/Core/WebView/TransitionManager.swift +169 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WeakScriptMessageHandler.swift +14 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +38 -7
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +6 -2
- package/dist/native/iosnativeWebView/iosnativeWebView/ContentView.swift +15 -4
- package/dist/native/iosnativeWebView/iosnativeWebView/Info.plist +11 -17
- package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +8 -2
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/PluginBridgeTests.swift +160 -0
- package/dist/native/plugin-bridge/PluginBridge.js +1 -0
- package/dist/native/pluginComposerAndroid.js +9 -0
- package/dist/native/pluginComposerIos.js +7 -0
- package/dist/native/renameAndroidProject.js +3 -3
- package/dist/native/setupServer.js +2 -3
- package/dist/native/utils.js +4 -1
- package/dist/otel.js +70 -2
- package/dist/scripts/devServe.js +1 -1
- package/dist/scripts/plugins.js +1 -0
- package/dist/scripts/registerAliases.js +6 -2
- package/dist/scripts/scriptUtils.js +2 -1
- package/dist/scripts/serve.js +2 -1
- package/dist/scripts/start.js +4 -1
- package/dist/server/renderer/extract.js +5 -5
- package/dist/server/renderer/handler.js +15 -14
- package/dist/server/utils/validator.js +1 -1
- package/dist/webpack/base.babel.js +2 -1
- package/dist/webpack/development.client.babel.js +6 -2
- package/dist/webpack/production.ssr.babel.js +5 -1
- package/mcp_v2/knowledge-base.json +1564 -1067
- package/mcp_v2/lib/helpers.js +19 -7
- package/mcp_v2/tools/build.js +1 -1
- package/mcp_v2/tools/config.js +16 -4
- package/mcp_v2/tools/conversion.js +11 -11
- package/mcp_v2/tools/knowledge.js +16 -3
- package/mcp_v2/tools/tasks.js +13 -4
- package/package.json +2 -2
|
@@ -17,11 +17,17 @@ class NativeBridge: NSObject, BridgeCommandHandlerDelegate, BridgeFileHandlerDel
|
|
|
17
17
|
weak var webView: WKWebView?
|
|
18
18
|
private weak var viewController: UIViewController?
|
|
19
19
|
private weak var webViewModel: WebViewModel?
|
|
20
|
+
private var cameraManager: NativeCameraManager?
|
|
21
|
+
private var messageHandlerProxy: WeakScriptMessageHandler?
|
|
22
|
+
private var isRegistered = false
|
|
20
23
|
|
|
21
24
|
// Protocol-based notification handler (injected at runtime)
|
|
22
25
|
private var notificationHandler: NotificationHandlerProtocol = NullNotificationHandler.shared
|
|
23
26
|
private var networkStatusListenerId: UUID?
|
|
24
27
|
|
|
28
|
+
// Transition manager — snapshot overlay pattern for native page transitions
|
|
29
|
+
private var transitionManager: TransitionManager?
|
|
30
|
+
|
|
25
31
|
// Lazy initialization for non-critical handlers
|
|
26
32
|
private lazy var imageHandler: ImageHandler = {
|
|
27
33
|
logWithTimestamp("🔧 ImageHandler initialized (lazy)")
|
|
@@ -68,17 +74,38 @@ class NativeBridge: NSObject, BridgeCommandHandlerDelegate, BridgeFileHandlerDel
|
|
|
68
74
|
return handler
|
|
69
75
|
}()
|
|
70
76
|
|
|
71
|
-
init(webView: WKWebView, viewController: UIViewController) {
|
|
77
|
+
init(webView: WKWebView, viewController: UIViewController, cameraManager: NativeCameraManager? = nil) {
|
|
72
78
|
let initStart = CFAbsoluteTimeGetCurrent()
|
|
73
79
|
|
|
74
80
|
self.webView = webView
|
|
75
81
|
self.viewController = viewController
|
|
82
|
+
self.cameraManager = cameraManager
|
|
76
83
|
|
|
77
84
|
// Only initialize critical JS interface immediately
|
|
78
85
|
self.jsInterface = BridgeJavaScriptInterface(webView: webView)
|
|
79
86
|
|
|
80
87
|
super.init()
|
|
81
88
|
|
|
89
|
+
// Wire camera events back through the bridge
|
|
90
|
+
cameraManager?.rewireEvents { [weak self] eventName, payload in
|
|
91
|
+
if let payload {
|
|
92
|
+
self?.sendJSONCallback(eventName: eventName, data: payload)
|
|
93
|
+
} else {
|
|
94
|
+
self?.sendCallback(eventName: eventName)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
cameraManager?.rewireError { [weak self] message in
|
|
98
|
+
self?.sendErrorCallback(eventName: "ON_CAMERA_ERROR", error: message, code: "CAMERA_ERROR")
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Wire transition manager
|
|
102
|
+
let tm = TransitionManager(webView: webView)
|
|
103
|
+
tm.onEvent = { [weak self] eventName in
|
|
104
|
+
self?.sendCallback(eventName: eventName)
|
|
105
|
+
}
|
|
106
|
+
self.transitionManager = tm
|
|
107
|
+
|
|
108
|
+
|
|
82
109
|
setupNotificationNavigationHandler()
|
|
83
110
|
|
|
84
111
|
let initTime = (CFAbsoluteTimeGetCurrent() - initStart) * 1000
|
|
@@ -133,10 +160,16 @@ class NativeBridge: NSObject, BridgeCommandHandlerDelegate, BridgeFileHandlerDel
|
|
|
133
160
|
|
|
134
161
|
// Register the JavaScript interface with the WebView
|
|
135
162
|
func register() {
|
|
163
|
+
guard !isRegistered else { return }
|
|
136
164
|
let registerStart = CFAbsoluteTimeGetCurrent()
|
|
137
165
|
|
|
138
|
-
let userContentController = webView?.configuration.userContentController
|
|
139
|
-
|
|
166
|
+
guard let userContentController = webView?.configuration.userContentController else {
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
let proxy = WeakScriptMessageHandler(delegate: self)
|
|
170
|
+
userContentController.add(proxy, name: "NativeBridge")
|
|
171
|
+
messageHandlerProxy = proxy
|
|
172
|
+
isRegistered = true
|
|
140
173
|
|
|
141
174
|
let registerTime = (CFAbsoluteTimeGetCurrent() - registerStart) * 1000
|
|
142
175
|
logWithTimestamp("✅ NativeBridge registered (took \(String(format: "%.2f", registerTime))ms)")
|
|
@@ -144,12 +177,19 @@ class NativeBridge: NSObject, BridgeCommandHandlerDelegate, BridgeFileHandlerDel
|
|
|
144
177
|
|
|
145
178
|
// Unregister to prevent memory leaks
|
|
146
179
|
func unregister() {
|
|
180
|
+
guard isRegistered else { return }
|
|
147
181
|
webView?.configuration.userContentController.removeScriptMessageHandler(forName: "NativeBridge")
|
|
148
|
-
|
|
182
|
+
messageHandlerProxy = nil
|
|
183
|
+
isRegistered = false
|
|
184
|
+
|
|
185
|
+
|
|
149
186
|
if let listenerId = networkStatusListenerId {
|
|
150
187
|
NetworkMonitor.shared.removeListener(listenerId)
|
|
151
188
|
networkStatusListenerId = nil
|
|
152
189
|
}
|
|
190
|
+
|
|
191
|
+
transitionManager?.cleanup()
|
|
192
|
+
transitionManager = nil
|
|
153
193
|
}
|
|
154
194
|
|
|
155
195
|
private func startNetworkMonitoringIfNeeded() {
|
|
@@ -282,6 +322,25 @@ extension NativeBridge: WKScriptMessageHandler {
|
|
|
282
322
|
commandHandler.unsubscribeFromTopic(config)
|
|
283
323
|
case "getSubscribedTopics":
|
|
284
324
|
commandHandler.getSubscribedTopics()
|
|
325
|
+
// Video stream commands
|
|
326
|
+
case "startVideoStream":
|
|
327
|
+
handleStartVideoStream(params: params)
|
|
328
|
+
case "stopVideoStream":
|
|
329
|
+
logger.info("📹 stopVideoStream received")
|
|
330
|
+
if cameraManager == nil { logger.error("📹 stopVideoStream — cameraManager is nil") }
|
|
331
|
+
cameraManager?.stop()
|
|
332
|
+
case "flipVideoStream":
|
|
333
|
+
logger.info("📹 flipVideoStream received")
|
|
334
|
+
if cameraManager == nil { logger.error("📹 flipVideoStream — cameraManager is nil") }
|
|
335
|
+
cameraManager?.flip()
|
|
336
|
+
case "sendVideoStreamCommand":
|
|
337
|
+
handleVideoStreamCommand(params: params)
|
|
338
|
+
case "setVideoStreamZoom":
|
|
339
|
+
handleSetVideoStreamZoom(params: params)
|
|
340
|
+
case "setVideoStreamTorch":
|
|
341
|
+
handleSetVideoStreamTorch(params: params)
|
|
342
|
+
case "setVideoStreamFps":
|
|
343
|
+
handleSetVideoStreamFps(params: params)
|
|
285
344
|
case "getSafeArea":
|
|
286
345
|
getSafeArea()
|
|
287
346
|
case "setScreenSecure":
|
|
@@ -290,12 +349,176 @@ extension NativeBridge: WKScriptMessageHandler {
|
|
|
290
349
|
commandHandler.getScreenSecure()
|
|
291
350
|
case "clearWebData":
|
|
292
351
|
commandHandler.clearWebData()
|
|
352
|
+
|
|
353
|
+
// Transition commands
|
|
354
|
+
case "startTransition":
|
|
355
|
+
let optionsString = delegateHandler.extractStringParam(from: params)
|
|
356
|
+
handleStartTransition(optionsString: optionsString)
|
|
357
|
+
case "commitTransition":
|
|
358
|
+
DispatchQueue.main.async { [weak self] in self?.transitionManager?.commitTransition() }
|
|
359
|
+
case "cancelTransition":
|
|
360
|
+
DispatchQueue.main.async { [weak self] in self?.transitionManager?.cancelTransition() }
|
|
361
|
+
|
|
293
362
|
default:
|
|
294
363
|
// This should never happen due to validation, but keeping for safety
|
|
295
364
|
logger.error("Unexpected command reached execution: \(command)")
|
|
296
365
|
}
|
|
297
366
|
}
|
|
298
367
|
|
|
368
|
+
// MARK: - Video Stream Helpers
|
|
369
|
+
|
|
370
|
+
/// JS always sends params as JSON.stringify(options) — a String — because Android requires it.
|
|
371
|
+
/// Parse it into a dictionary here rather than removing the stringify on the JS side.
|
|
372
|
+
private func jsonStringToDict(_ params: Any?) -> [String: Any]? {
|
|
373
|
+
// Fast path: WKWebView decoded it already (unlikely for video stream commands but handle gracefully)
|
|
374
|
+
if let d = params as? [String: Any] { return d }
|
|
375
|
+
if let d = (params as? NSDictionary) as? [String: Any] { return d }
|
|
376
|
+
// Normal path: params is a JSON string from JSON.stringify()
|
|
377
|
+
guard let str = params as? String,
|
|
378
|
+
let data = str.data(using: .utf8),
|
|
379
|
+
let obj = try? JSONSerialization.jsonObject(with: data),
|
|
380
|
+
let dict = obj as? [String: Any] else { return nil }
|
|
381
|
+
return dict
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private func handleStartVideoStream(params: Any?) {
|
|
385
|
+
logger.info("📹 startVideoStream received — raw params type: \(type(of: params))")
|
|
386
|
+
guard let cam = cameraManager else {
|
|
387
|
+
logger.error("📹 startVideoStream — cameraManager is nil")
|
|
388
|
+
return
|
|
389
|
+
}
|
|
390
|
+
var facing = "back"
|
|
391
|
+
var autoZoom = false
|
|
392
|
+
var initialZoom: Float = 1.0
|
|
393
|
+
var scanFormat = "all"
|
|
394
|
+
var fpsMin: Int? = nil
|
|
395
|
+
var fpsMax: Int? = nil
|
|
396
|
+
|
|
397
|
+
if let dict = jsonStringToDict(params) {
|
|
398
|
+
logger.info("📹 startVideoStream — dict keys: \(dict.keys.sorted().joined(separator: ", "))")
|
|
399
|
+
facing = dict["facing"] as? String ?? facing
|
|
400
|
+
autoZoom = dict["autoZoom"] as? Bool ?? autoZoom
|
|
401
|
+
scanFormat = dict["format"] as? String ?? scanFormat
|
|
402
|
+
// zoom options are nested under 'zoom' key: { auto: Bool, initial: Float }
|
|
403
|
+
if let zoomDict = dict["zoom"] as? [String: Any] {
|
|
404
|
+
autoZoom = (zoomDict["auto"] as? NSNumber)?.boolValue ?? autoZoom
|
|
405
|
+
initialZoom = (zoomDict["initial"] as? NSNumber)?.floatValue ?? initialZoom
|
|
406
|
+
}
|
|
407
|
+
if let fps = dict["fps"] as? [String: Any] {
|
|
408
|
+
fpsMin = fps["min"] as? Int
|
|
409
|
+
fpsMax = fps["max"] as? Int
|
|
410
|
+
}
|
|
411
|
+
} else {
|
|
412
|
+
logger.warning("📹 startVideoStream — could not parse params (type=\(type(of: params))), using defaults")
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
logger.info("📹 startVideoStream — starting with facing=\(facing) autoZoom=\(autoZoom) initialZoom=\(initialZoom) format=\(scanFormat) fpsMin=\(String(describing: fpsMin)) fpsMax=\(String(describing: fpsMax))")
|
|
416
|
+
cam.start(facing: facing, autoZoom: autoZoom, initialZoom: initialZoom,
|
|
417
|
+
scanFormat: scanFormat, fpsMin: fpsMin, fpsMax: fpsMax)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private func handleVideoStreamCommand(params: Any?) {
|
|
421
|
+
guard let cam = cameraManager,
|
|
422
|
+
let dict = jsonStringToDict(params),
|
|
423
|
+
let type = dict["type"] as? String else {
|
|
424
|
+
logger.error("📹 handleVideoStreamCommand — guard failed: cameraManager=\(self.cameraManager != nil) params=\(String(describing: params))")
|
|
425
|
+
return
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
switch type {
|
|
429
|
+
case "zoom":
|
|
430
|
+
if let value = dict["value"] as? NSNumber {
|
|
431
|
+
cam.setZoom(multiplier: value.floatValue)
|
|
432
|
+
}
|
|
433
|
+
case "torch":
|
|
434
|
+
if let value = dict["value"] as? Bool {
|
|
435
|
+
cam.setTorch(value)
|
|
436
|
+
}
|
|
437
|
+
case "fps":
|
|
438
|
+
let fpsDict = dict["value"] as? [String: Any]
|
|
439
|
+
let min = fpsDict?["min"] as? Int
|
|
440
|
+
let max = fpsDict?["max"] as? Int
|
|
441
|
+
cam.setFps(min: min, max: max)
|
|
442
|
+
default:
|
|
443
|
+
logger.warning("Unknown videoStream command type: \(type)")
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
private func handleSetVideoStreamZoom(params: Any?) {
|
|
448
|
+
logger.info("📹 setVideoStreamZoom received — raw params: \(String(describing: params))")
|
|
449
|
+
guard let cam = cameraManager else {
|
|
450
|
+
logger.error("📹 setVideoStreamZoom — cameraManager is nil")
|
|
451
|
+
return
|
|
452
|
+
}
|
|
453
|
+
guard let raw = params as? String, let multiplier = Float(raw) else {
|
|
454
|
+
logger.error("📹 setVideoStreamZoom — expected String number, got: \(String(describing: params))")
|
|
455
|
+
return
|
|
456
|
+
}
|
|
457
|
+
logger.info("📹 setVideoStreamZoom — multiplier=\(multiplier)x")
|
|
458
|
+
cam.setZoom(multiplier: multiplier)
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private func handleSetVideoStreamTorch(params: Any?) {
|
|
462
|
+
logger.info("📹 setVideoStreamTorch received — raw params: \(String(describing: params))")
|
|
463
|
+
guard let cam = cameraManager else {
|
|
464
|
+
logger.error("📹 setVideoStreamTorch — cameraManager is nil")
|
|
465
|
+
return
|
|
466
|
+
}
|
|
467
|
+
// JS sends JSON.stringify({on: bool})
|
|
468
|
+
guard let dict = jsonStringToDict(params), let on = dict["on"] as? Bool else {
|
|
469
|
+
logger.error("📹 setVideoStreamTorch — expected JSON string {on: Bool}, got: \(String(describing: params))")
|
|
470
|
+
return
|
|
471
|
+
}
|
|
472
|
+
logger.info("📹 setVideoStreamTorch — on=\(on)")
|
|
473
|
+
cam.setTorch(on)
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
private func handleSetVideoStreamFps(params: Any?) {
|
|
477
|
+
logger.info("📹 setVideoStreamFps received — raw params: \(String(describing: params))")
|
|
478
|
+
guard let cam = cameraManager else {
|
|
479
|
+
logger.error("📹 setVideoStreamFps — cameraManager is nil")
|
|
480
|
+
return
|
|
481
|
+
}
|
|
482
|
+
// JS sends JSON.stringify({min, max})
|
|
483
|
+
guard let dict = jsonStringToDict(params) else {
|
|
484
|
+
logger.error("📹 setVideoStreamFps — expected JSON string {min, max}, got: \(String(describing: params))")
|
|
485
|
+
return
|
|
486
|
+
}
|
|
487
|
+
let min = dict["min"] as? Int
|
|
488
|
+
let max = dict["max"] as? Int
|
|
489
|
+
logger.info("📹 setVideoStreamFps — min=\(String(describing: min)) max=\(String(describing: max))")
|
|
490
|
+
cam.setFps(min: min, max: max)
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// MARK: - Transition Helpers
|
|
494
|
+
|
|
495
|
+
private func handleStartTransition(optionsString: String?) {
|
|
496
|
+
let options: [String: Any]
|
|
497
|
+
if let str = optionsString,
|
|
498
|
+
let data = str.data(using: .utf8),
|
|
499
|
+
let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
|
|
500
|
+
options = obj
|
|
501
|
+
} else {
|
|
502
|
+
options = [:]
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
let type = options["type"] as? String ?? "slide"
|
|
506
|
+
let direction = options["direction"] as? String ?? "left"
|
|
507
|
+
let duration = min(max((options["duration"] as? Int ?? 300), 0), 2000)
|
|
508
|
+
let defaultTimeout = max(duration * 3, 800)
|
|
509
|
+
let timeout = min(max((options["timeout"] as? Int ?? defaultTimeout), 100), 10000)
|
|
510
|
+
|
|
511
|
+
DispatchQueue.main.async { [weak self] in
|
|
512
|
+
self?.transitionManager?.startTransition(
|
|
513
|
+
type: type,
|
|
514
|
+
direction: direction,
|
|
515
|
+
duration: duration,
|
|
516
|
+
timeout: timeout
|
|
517
|
+
)
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
|
|
299
522
|
// MARK: - Safe Area Methods
|
|
300
523
|
|
|
301
524
|
/// Get current safe area insets and send via callback
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
import WebKit
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
private let transitionLogger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.app", category: "TransitionManager")
|
|
6
|
+
|
|
7
|
+
/// Manages native page transitions using a snapshot-overlay pattern.
|
|
8
|
+
///
|
|
9
|
+
/// Flow:
|
|
10
|
+
/// startTransition() — captures a snapshotView of the current WebView, places it as an
|
|
11
|
+
/// overlay on the key window covering the skeleton rendered by the router.
|
|
12
|
+
/// commitTransition() — animates the overlay out (slide or fade) to reveal the new page.
|
|
13
|
+
/// cancelTransition() — removes the overlay immediately without animating.
|
|
14
|
+
///
|
|
15
|
+
/// Safety timer: a DispatchWorkItem fires after `timeout` ms if commitTransition() was
|
|
16
|
+
/// never called. It force-removes the overlay and fires ON_TRANSITION_TIMEOUT to JS.
|
|
17
|
+
///
|
|
18
|
+
/// Thread safety: all public methods must be called on the main thread.
|
|
19
|
+
class TransitionManager {
|
|
20
|
+
|
|
21
|
+
private weak var webView: WKWebView?
|
|
22
|
+
private var overlayView: UIView?
|
|
23
|
+
private var safetyItem: DispatchWorkItem?
|
|
24
|
+
private var activeDuration: TimeInterval = 0.3
|
|
25
|
+
private var activeType: String = "slide"
|
|
26
|
+
private var activeDirection: String = "left"
|
|
27
|
+
|
|
28
|
+
// Callback to fire JS events — injected by NativeBridge
|
|
29
|
+
var onEvent: (String) -> Void = { _ in }
|
|
30
|
+
|
|
31
|
+
init(webView: WKWebView) {
|
|
32
|
+
self.webView = webView
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// MARK: - Public API
|
|
36
|
+
|
|
37
|
+
func startTransition(type: String, direction: String, duration: Int, timeout: Int) {
|
|
38
|
+
// Cancel any orphaned transition first
|
|
39
|
+
cancelTransitionInternal(notify: false)
|
|
40
|
+
|
|
41
|
+
activeType = type
|
|
42
|
+
activeDirection = direction
|
|
43
|
+
activeDuration = Double(duration) / 1000.0
|
|
44
|
+
|
|
45
|
+
guard let snapshot = captureSnapshot() else {
|
|
46
|
+
transitionLogger.warning("startTransition — snapshot failed, transition skipped")
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
guard let window = webView?.window ?? UIApplication.shared.connectedScenes
|
|
51
|
+
.compactMap({ ($0 as? UIWindowScene)?.keyWindow })
|
|
52
|
+
.first else {
|
|
53
|
+
transitionLogger.warning("startTransition — no key window found")
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
snapshot.frame = window.bounds
|
|
58
|
+
snapshot.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
59
|
+
window.addSubview(snapshot)
|
|
60
|
+
overlayView = snapshot
|
|
61
|
+
|
|
62
|
+
transitionLogger.debug("startTransition — overlay attached (type=\(type) direction=\(direction) duration=\(duration)ms timeout=\(timeout)ms)")
|
|
63
|
+
|
|
64
|
+
// Arm safety timer
|
|
65
|
+
let item = DispatchWorkItem { [weak self] in
|
|
66
|
+
transitionLogger.warning("startTransition — safety timeout fired, force-removing overlay")
|
|
67
|
+
self?.cancelTransitionInternal(notify: false)
|
|
68
|
+
self?.onEvent("ON_TRANSITION_TIMEOUT")
|
|
69
|
+
}
|
|
70
|
+
safetyItem = item
|
|
71
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(timeout), execute: item)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
func commitTransition() {
|
|
75
|
+
guard let overlay = overlayView else {
|
|
76
|
+
transitionLogger.warning("commitTransition — no active overlay, ignoring")
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
disarmSafetyTimer()
|
|
81
|
+
|
|
82
|
+
switch activeType {
|
|
83
|
+
case "fade":
|
|
84
|
+
animateFadeOut(overlay: overlay, duration: activeDuration)
|
|
85
|
+
default:
|
|
86
|
+
animateSlideOut(overlay: overlay, duration: activeDuration, direction: activeDirection)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
func cancelTransition() {
|
|
91
|
+
cancelTransitionInternal(notify: true)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
func cleanup() {
|
|
95
|
+
cancelTransitionInternal(notify: false)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// MARK: - Private helpers
|
|
99
|
+
|
|
100
|
+
private func captureSnapshot() -> UIView? {
|
|
101
|
+
guard let webView else { return nil }
|
|
102
|
+
// snapshotView is GPU-backed and fast — no bitmap copy needed on iOS
|
|
103
|
+
return webView.snapshotView(afterScreenUpdates: false)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private func animateSlideOut(overlay: UIView, duration: TimeInterval, direction: String) {
|
|
107
|
+
guard let window = overlay.superview else { return }
|
|
108
|
+
let w = window.bounds.width
|
|
109
|
+
let h = window.bounds.height
|
|
110
|
+
|
|
111
|
+
let endTranslation: CGPoint
|
|
112
|
+
switch direction {
|
|
113
|
+
case "right": endTranslation = CGPoint(x: -w, y: 0)
|
|
114
|
+
case "up": endTranslation = CGPoint(x: 0, y: h)
|
|
115
|
+
case "down": endTranslation = CGPoint(x: 0, y: -h)
|
|
116
|
+
default: endTranslation = CGPoint(x: w, y: 0) // "left" — overlay slides out to the right
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
UIView.animate(
|
|
120
|
+
withDuration: duration,
|
|
121
|
+
delay: 0,
|
|
122
|
+
options: [.curveEaseInOut],
|
|
123
|
+
animations: {
|
|
124
|
+
overlay.transform = CGAffineTransform(translationX: endTranslation.x, y: endTranslation.y)
|
|
125
|
+
},
|
|
126
|
+
completion: { [weak self] _ in
|
|
127
|
+
self?.removeOverlay(overlay)
|
|
128
|
+
self?.onEvent("ON_TRANSITION_COMMITTED")
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private func animateFadeOut(overlay: UIView, duration: TimeInterval) {
|
|
134
|
+
UIView.animate(
|
|
135
|
+
withDuration: duration,
|
|
136
|
+
delay: 0,
|
|
137
|
+
options: [.curveEaseInOut],
|
|
138
|
+
animations: {
|
|
139
|
+
overlay.alpha = 0
|
|
140
|
+
},
|
|
141
|
+
completion: { [weak self] _ in
|
|
142
|
+
self?.removeOverlay(overlay)
|
|
143
|
+
self?.onEvent("ON_TRANSITION_COMMITTED")
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private func cancelTransitionInternal(notify: Bool) {
|
|
149
|
+
disarmSafetyTimer()
|
|
150
|
+
if let overlay = overlayView {
|
|
151
|
+
removeOverlay(overlay)
|
|
152
|
+
if notify {
|
|
153
|
+
onEvent("ON_TRANSITION_CANCELLED")
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private func removeOverlay(_ overlay: UIView) {
|
|
159
|
+
overlay.removeFromSuperview()
|
|
160
|
+
if overlayView === overlay {
|
|
161
|
+
overlayView = nil
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private func disarmSafetyTimer() {
|
|
166
|
+
safetyItem?.cancel()
|
|
167
|
+
safetyItem = nil
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import WebKit
|
|
2
|
+
|
|
3
|
+
final class WeakScriptMessageHandler: NSObject, WKScriptMessageHandler {
|
|
4
|
+
weak var delegate: WKScriptMessageHandler?
|
|
5
|
+
|
|
6
|
+
init(delegate: WKScriptMessageHandler) {
|
|
7
|
+
self.delegate = delegate
|
|
8
|
+
super.init()
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
|
12
|
+
delegate?.userContentController(userContentController, didReceive: message)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -7,6 +7,7 @@ private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.app"
|
|
|
7
7
|
public struct WebView: UIViewRepresentable, Equatable {
|
|
8
8
|
let urlString: String
|
|
9
9
|
@ObservedObject var viewModel: WebViewModel
|
|
10
|
+
let cameraManager: NativeCameraManager
|
|
10
11
|
private let navigationDelegate: WebViewNavigationDelegate
|
|
11
12
|
|
|
12
13
|
// Equatable conformance - only recreate if URL changes
|
|
@@ -14,12 +15,13 @@ public struct WebView: UIViewRepresentable, Equatable {
|
|
|
14
15
|
return lhs.urlString == rhs.urlString
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
public init(urlString: String, viewModel: WebViewModel) {
|
|
18
|
+
public init(urlString: String, viewModel: WebViewModel, cameraManager: NativeCameraManager) {
|
|
18
19
|
let start = CFAbsoluteTimeGetCurrent()
|
|
19
20
|
self.urlString = urlString
|
|
20
21
|
self.viewModel = viewModel
|
|
22
|
+
self.cameraManager = cameraManager
|
|
21
23
|
let initialURL = URL(string: urlString)
|
|
22
|
-
self.navigationDelegate = WebViewNavigationDelegate(viewModel: viewModel, initialURL: initialURL)
|
|
24
|
+
self.navigationDelegate = WebViewNavigationDelegate(viewModel: viewModel, initialURL: initialURL, cameraManager: cameraManager)
|
|
23
25
|
|
|
24
26
|
// Register our custom URL protocol for caching
|
|
25
27
|
let protocolStart = CFAbsoluteTimeGetCurrent()
|
|
@@ -40,9 +42,6 @@ public struct WebView: UIViewRepresentable, Equatable {
|
|
|
40
42
|
// Hook kept for legacy behavior; currently a no-op on iOS 15+.
|
|
41
43
|
WebKitConfig.applySharedProcessPoolIfNeeded(to: configuration)
|
|
42
44
|
|
|
43
|
-
#if DEBUG
|
|
44
|
-
configuration.preferences.setValue(true, forKey: "developerExtrasEnabled")
|
|
45
|
-
#endif
|
|
46
45
|
let preferences = WKWebpagePreferences()
|
|
47
46
|
preferences.allowsContentJavaScript = true
|
|
48
47
|
configuration.defaultWebpagePreferences = preferences
|
|
@@ -54,6 +53,15 @@ public struct WebView: UIViewRepresentable, Equatable {
|
|
|
54
53
|
|
|
55
54
|
webView.navigationDelegate = navigationDelegate
|
|
56
55
|
webView.allowsBackForwardNavigationGestures = true
|
|
56
|
+
webView.isOpaque = false
|
|
57
|
+
webView.backgroundColor = .clear
|
|
58
|
+
webView.scrollView.backgroundColor = .clear
|
|
59
|
+
|
|
60
|
+
// Add our pinch recognizer to route camera zoom when streaming.
|
|
61
|
+
// Web page zoom is disabled via user-scalable=no in the viewport meta tag.
|
|
62
|
+
let pinchGesture = UIPinchGestureRecognizer(target: context.coordinator,
|
|
63
|
+
action: #selector(Coordinator.handleCameraPinch(_:)))
|
|
64
|
+
webView.addGestureRecognizer(pinchGesture)
|
|
57
65
|
|
|
58
66
|
#if DEBUG
|
|
59
67
|
// Enable Safari Web Inspector (only available in iOS 16.4+)
|
|
@@ -101,7 +109,11 @@ public struct WebView: UIViewRepresentable, Equatable {
|
|
|
101
109
|
}
|
|
102
110
|
|
|
103
111
|
public func updateUIView(_ webView: WKWebView, context: Context) {
|
|
104
|
-
|
|
112
|
+
#if DEBUG
|
|
113
|
+
if #available(iOS 16.4, *) {
|
|
114
|
+
webView.isInspectable = true
|
|
115
|
+
}
|
|
116
|
+
#endif
|
|
105
117
|
}
|
|
106
118
|
|
|
107
119
|
public func makeCoordinator() -> Coordinator {
|
|
@@ -121,6 +133,8 @@ public struct WebView: UIViewRepresentable, Equatable {
|
|
|
121
133
|
// Clean up native bridge
|
|
122
134
|
coordinator.nativeBridge?.unregister()
|
|
123
135
|
coordinator.nativeBridge = nil
|
|
136
|
+
coordinator.pluginBridge?.unregister()
|
|
137
|
+
coordinator.pluginBridge = nil
|
|
124
138
|
coordinator.hostingController = nil
|
|
125
139
|
|
|
126
140
|
// Unregister custom URL protocol
|
|
@@ -132,6 +146,7 @@ public struct WebView: UIViewRepresentable, Equatable {
|
|
|
132
146
|
public class Coordinator: NSObject {
|
|
133
147
|
var parent: WebView
|
|
134
148
|
var nativeBridge: NativeBridge?
|
|
149
|
+
var pluginBridge: PluginBridge?
|
|
135
150
|
var hostingController: UIViewController?
|
|
136
151
|
var isObserverAdded = false
|
|
137
152
|
|
|
@@ -145,7 +160,8 @@ public struct WebView: UIViewRepresentable, Equatable {
|
|
|
145
160
|
self.hostingController = hostingController
|
|
146
161
|
|
|
147
162
|
// Create and register the native bridge
|
|
148
|
-
let bridge = NativeBridge(webView: webView, viewController: hostingController)
|
|
163
|
+
let bridge = NativeBridge(webView: webView, viewController: hostingController, cameraManager: parent.cameraManager)
|
|
164
|
+
let pluginBridge = PluginBridge(webView: webView, viewController: hostingController)
|
|
149
165
|
|
|
150
166
|
// Inject WebViewModel for safe area handling
|
|
151
167
|
Task { @MainActor in
|
|
@@ -156,9 +172,24 @@ public struct WebView: UIViewRepresentable, Equatable {
|
|
|
156
172
|
bridge.setNotificationHandler(NotificationHandlerProvider.shared)
|
|
157
173
|
|
|
158
174
|
bridge.register()
|
|
175
|
+
pluginBridge.register()
|
|
159
176
|
self.nativeBridge = bridge
|
|
177
|
+
self.pluginBridge = pluginBridge
|
|
160
178
|
}
|
|
161
179
|
|
|
180
|
+
@objc func handleCameraPinch(_ gesture: UIPinchGestureRecognizer) {
|
|
181
|
+
// No-op when camera is not streaming — don't accidentally zoom the page
|
|
182
|
+
guard parent.cameraManager.isStreaming else { return }
|
|
183
|
+
switch gesture.state {
|
|
184
|
+
case .began:
|
|
185
|
+
parent.cameraManager.handlePinchBegan()
|
|
186
|
+
case .changed:
|
|
187
|
+
parent.cameraManager.handlePinchChanged(scale: gesture.scale)
|
|
188
|
+
default:
|
|
189
|
+
break
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
162
193
|
override public func observeValue(forKeyPath keyPath: String?,
|
|
163
194
|
of object: Any?,
|
|
164
195
|
change: [NSKeyValueChangeKey : Any]?,
|
|
@@ -11,11 +11,13 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
|
|
|
11
11
|
private var offlinePageVisible = false
|
|
12
12
|
private var lastTargetURL: URL?
|
|
13
13
|
private let initialURL: URL?
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
private weak var cameraManager: NativeCameraManager?
|
|
15
|
+
|
|
16
|
+
init(viewModel: WebViewModel, initialURL: URL?, cameraManager: NativeCameraManager? = nil) {
|
|
16
17
|
self.viewModel = viewModel
|
|
17
18
|
self.initialURL = initialURL
|
|
18
19
|
self.lastTargetURL = initialURL
|
|
20
|
+
self.cameraManager = cameraManager
|
|
19
21
|
super.init()
|
|
20
22
|
}
|
|
21
23
|
|
|
@@ -199,6 +201,8 @@ class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
|
|
|
199
201
|
|
|
200
202
|
func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) {
|
|
201
203
|
logWithTimestamp("📡 didStartProvisionalNavigation - loading started")
|
|
204
|
+
// Stop camera on navigation — mirrors Android CustomWebview onPageStarted lambda
|
|
205
|
+
cameraManager?.stop()
|
|
202
206
|
Task { @MainActor in
|
|
203
207
|
if !isOfflinePageURL(webView.url) {
|
|
204
208
|
offlinePageVisible = false
|
|
@@ -7,21 +7,31 @@ private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.app"
|
|
|
7
7
|
|
|
8
8
|
public struct ContentView: View {
|
|
9
9
|
@StateObject private var webViewModel = WebViewModel()
|
|
10
|
-
|
|
10
|
+
// @StateObject ensures cameraManager survives SwiftUI view rebuilds.
|
|
11
|
+
@StateObject private var cameraManager = NativeCameraManager(
|
|
12
|
+
onEvent: { _, _ in }, // placeholder; real handler set in NativeBridge
|
|
13
|
+
onError: { _ in }
|
|
14
|
+
)
|
|
15
|
+
|
|
11
16
|
public init() {}
|
|
12
17
|
|
|
13
18
|
public var body: some View {
|
|
14
19
|
ZStack {
|
|
20
|
+
// Camera preview layer — sits behind the WebView (index 0)
|
|
21
|
+
CameraPreviewView(cameraManager: cameraManager)
|
|
22
|
+
.ignoresSafeArea()
|
|
23
|
+
.allowsHitTesting(false) // touches pass through to WebView
|
|
24
|
+
|
|
15
25
|
// Normal remote URL - isolated from state changes
|
|
16
26
|
// Conditionally apply edge-to-edge based on config (matches Android behavior)
|
|
17
27
|
if ConfigConstants.EdgeToEdge.enabled {
|
|
18
|
-
WebViewContainer(urlString: ConfigConstants.url, viewModel: webViewModel)
|
|
28
|
+
WebViewContainer(urlString: ConfigConstants.url, viewModel: webViewModel, cameraManager: cameraManager)
|
|
19
29
|
.ignoresSafeArea()
|
|
20
30
|
.onAppear {
|
|
21
31
|
logger.info("WebView appeared with URL: \(ConfigConstants.url) [Edge-to-edge: enabled]")
|
|
22
32
|
}
|
|
23
33
|
} else {
|
|
24
|
-
WebViewContainer(urlString: ConfigConstants.url, viewModel: webViewModel)
|
|
34
|
+
WebViewContainer(urlString: ConfigConstants.url, viewModel: webViewModel, cameraManager: cameraManager)
|
|
25
35
|
.onAppear {
|
|
26
36
|
logger.info("WebView appeared with URL: \(ConfigConstants.url) [Edge-to-edge: disabled, respecting safe areas]")
|
|
27
37
|
}
|
|
@@ -62,9 +72,10 @@ public struct ContentView: View {
|
|
|
62
72
|
struct WebViewContainer: View {
|
|
63
73
|
let urlString: String
|
|
64
74
|
@ObservedObject var viewModel: WebViewModel
|
|
75
|
+
let cameraManager: NativeCameraManager
|
|
65
76
|
|
|
66
77
|
var body: some View {
|
|
67
|
-
WebView(urlString: urlString, viewModel: viewModel)
|
|
78
|
+
WebView(urlString: urlString, viewModel: viewModel, cameraManager: cameraManager)
|
|
68
79
|
.onAppear {
|
|
69
80
|
logWithTimestamp("🌐 WebView appeared with URL: \(urlString)")
|
|
70
81
|
}
|