@quicktvui/web-renderer 1.0.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 (56) hide show
  1. package/package.json +24 -0
  2. package/src/adapters/es3-video-player.js +828 -0
  3. package/src/components/Modal.js +119 -0
  4. package/src/components/QtAnimationView.js +678 -0
  5. package/src/components/QtBaseComponent.js +165 -0
  6. package/src/components/QtFastListView.js +1920 -0
  7. package/src/components/QtFlexView.js +799 -0
  8. package/src/components/QtImage.js +203 -0
  9. package/src/components/QtItemFrame.js +239 -0
  10. package/src/components/QtItemStoreView.js +93 -0
  11. package/src/components/QtItemView.js +125 -0
  12. package/src/components/QtListView.js +331 -0
  13. package/src/components/QtLoadingView.js +55 -0
  14. package/src/components/QtPageRootView.js +19 -0
  15. package/src/components/QtPlayMark.js +168 -0
  16. package/src/components/QtProgressBar.js +199 -0
  17. package/src/components/QtQRCode.js +78 -0
  18. package/src/components/QtReplaceChild.js +149 -0
  19. package/src/components/QtRippleView.js +166 -0
  20. package/src/components/QtSeekBar.js +409 -0
  21. package/src/components/QtText.js +679 -0
  22. package/src/components/QtTransitionImage.js +170 -0
  23. package/src/components/QtView.js +706 -0
  24. package/src/components/QtWebView.js +613 -0
  25. package/src/components/TabsView.js +420 -0
  26. package/src/components/ViewPager.js +206 -0
  27. package/src/components/index.js +24 -0
  28. package/src/components/plugins/TextV2Component.js +70 -0
  29. package/src/components/plugins/index.js +7 -0
  30. package/src/core/SceneBuilder.js +58 -0
  31. package/src/core/TVFocusManager.js +2014 -0
  32. package/src/core/asyncLocalStorage.js +175 -0
  33. package/src/core/autoProxy.js +165 -0
  34. package/src/core/componentRegistry.js +84 -0
  35. package/src/core/constants.js +6 -0
  36. package/src/core/index.js +8 -0
  37. package/src/core/moduleUtils.js +36 -0
  38. package/src/core/patches.js +958 -0
  39. package/src/core/templateBinding.js +666 -0
  40. package/src/index.js +246 -0
  41. package/src/modules/AndroidDevelopModule.js +101 -0
  42. package/src/modules/AndroidDeviceModule.js +341 -0
  43. package/src/modules/AndroidNetworkModule.js +178 -0
  44. package/src/modules/AndroidSharedPreferencesModule.js +100 -0
  45. package/src/modules/ESDeviceInfoModule.js +450 -0
  46. package/src/modules/ESGroupDataModule.js +195 -0
  47. package/src/modules/ESIJKAudioPlayerModule.js +477 -0
  48. package/src/modules/ESLocalStorageModule.js +100 -0
  49. package/src/modules/ESLogModule.js +65 -0
  50. package/src/modules/ESModule.js +106 -0
  51. package/src/modules/ESNetworkSpeedModule.js +117 -0
  52. package/src/modules/ESToastModule.js +172 -0
  53. package/src/modules/EsNativeModule.js +117 -0
  54. package/src/modules/FastListModule.js +101 -0
  55. package/src/modules/FocusModule.js +145 -0
  56. package/src/modules/RuntimeDeviceModule.js +176 -0
@@ -0,0 +1,958 @@
1
+ // Patches for web renderer
2
+ // Contains various patches and fixes for Hippy web-renderer
3
+
4
+ import { setWebEngine, findComponentById } from './componentRegistry'
5
+ import { TV_WIDTH, TV_HEIGHT } from './constants'
6
+ import { AndroidDeviceModule } from '../modules/AndroidDeviceModule'
7
+ import { androidNetworkModuleInstance as AndroidNetworkModule } from '../modules/AndroidNetworkModule'
8
+ import { runtimeDeviceModuleInstance as RuntimeDeviceModule } from '../modules/RuntimeDeviceModule'
9
+ import { esToastModuleInstance as ESToastModule } from '../modules/ESToastModule'
10
+ import { ESIJKAudioPlayerModule } from '../modules/ESIJKAudioPlayerModule'
11
+ import { esNetworkSpeedModuleInstance as ESNetworkSpeedModule } from '../modules/ESNetworkSpeedModule'
12
+
13
+ // Create singleton instance for audio player
14
+ const audioPlayerModule = new ESIJKAudioPlayerModule(null)
15
+ global.__WEB_MODULES__ = global.__WEB_MODULES__ || {}
16
+ global.__WEB_MODULES__['ESIJKAudioPlayerModule'] = audioPlayerModule
17
+ global.__WEB_MODULES__['ESNetworkSpeedModule'] = ESNetworkSpeedModule
18
+
19
+ // Track root node ID
20
+ let rootNodeId = null
21
+ let callNativePatched = false
22
+ let hippyCallNativesPatched = false
23
+
24
+ export function patchHippyCallNatives(engine) {
25
+ if (hippyCallNativesPatched) {
26
+ return
27
+ }
28
+ const original = global.hippyCallNatives
29
+ if (typeof original !== 'function') {
30
+ return
31
+ }
32
+
33
+ const storageMethods = new Set([
34
+ 'initSharedPreferences',
35
+ 'initESSharedPreferences',
36
+ 'getBoolean',
37
+ 'putBoolean',
38
+ 'getInt',
39
+ 'putInt',
40
+ 'getLong',
41
+ 'putLong',
42
+ 'getString',
43
+ 'putString',
44
+ 'getItem',
45
+ 'setItem',
46
+ ])
47
+
48
+ const esMethods = new Set([
49
+ 'getESDeviceInfo',
50
+ 'getESSDKInfo',
51
+ 'getESSDKVersionCode',
52
+ 'getESSDKVersionName',
53
+ 'getESPackageName',
54
+ 'getESAppFilePath',
55
+ 'getESAppRuntimePath',
56
+ 'getSupportSchemes',
57
+ 'launchESPageByArgs',
58
+ 'launchESPage',
59
+ 'launchNativeApp',
60
+ 'launchNativeAppWithPackage',
61
+ 'finish',
62
+ 'finishAll',
63
+ 'isModuleRegistered',
64
+ 'isComponentRegistered',
65
+ ])
66
+
67
+ // ESDevelop 模块方法
68
+ const developMethods = new Set([
69
+ 'getDevelop',
70
+ 'getPackageName',
71
+ 'getVersionName',
72
+ 'getVersionCode',
73
+ 'getChannel',
74
+ 'getVendor',
75
+ ])
76
+
77
+ global.hippyCallNatives = (moduleName, methodName, callId, params) => {
78
+ console.log('hippyCallNatives', moduleName, methodName, callId, params)
79
+
80
+ // Handle ExtendModule.callUIFunction - support sid-based component lookup
81
+ if (moduleName === 'ExtendModule' && methodName === 'callUIFunction') {
82
+ // params format: [sid, funcName, args]
83
+ const [sid, funcName, args] = params || []
84
+ console.log('[Web Renderer] ExtendModule.callUIFunction:', { sid, funcName, args })
85
+
86
+ if (!sid || !funcName) {
87
+ console.warn('[Web Renderer] ExtendModule.callUIFunction missing sid or funcName')
88
+ return
89
+ }
90
+
91
+ // Find component by sid
92
+ const component = findComponentById(sid)
93
+ if (component && typeof component[funcName] === 'function') {
94
+ console.log('[Web Renderer] Calling', funcName, 'on component with sid:', sid)
95
+ try {
96
+ const result = component[funcName](...(args || []))
97
+ return result
98
+ } catch (e) {
99
+ console.error('[Web Renderer] Error calling', funcName, ':', e)
100
+ }
101
+ } else if (component) {
102
+ console.warn('[Web Renderer] Component with sid:', sid, 'has no method:', funcName)
103
+ } else {
104
+ console.warn('[Web Renderer] Component not found for sid:', sid)
105
+ }
106
+ return
107
+ }
108
+
109
+ // Handle ESIJKAudioPlayerModule - redirect to web audio adapter
110
+ if (moduleName === 'ESIJKAudioPlayerModule') {
111
+ const audioModule = global.__WEB_MODULES__?.['ESIJKAudioPlayerModule']
112
+ if (audioModule && typeof audioModule[methodName] === 'function') {
113
+ console.log(
114
+ '[Web Renderer] ESIJKAudioPlayerModule (hippyCallNatives):',
115
+ methodName,
116
+ 'callId:',
117
+ callId,
118
+ 'params:',
119
+ params
120
+ )
121
+ // callId is the player id, params is array of additional args
122
+ const result = audioModule[methodName](callId, ...(params || [])).catch((e) => {
123
+ console.warn('[Web Renderer] ESIJKAudioPlayerModule error:', e)
124
+ })
125
+ return result
126
+ }
127
+ }
128
+
129
+ const hasModule = !!engine?.modules?.[moduleName]
130
+ if (!hasModule) {
131
+ console.error(`[Web Renderer] Module ${moduleName} not found!`)
132
+ if (storageMethods.has(methodName)) {
133
+ return original('ESLocalStorageModule', methodName, callId, params)
134
+ }
135
+ if (esMethods.has(methodName)) {
136
+ return original('ESModule', methodName, callId, params)
137
+ }
138
+ if (developMethods.has(methodName)) {
139
+ return original('AndroidDevelopModule', methodName, callId, params)
140
+ }
141
+ }
142
+ return original(moduleName, methodName, callId, params)
143
+ }
144
+
145
+ hippyCallNativesPatched = true
146
+ }
147
+
148
+ // Patch Hippy.bridge.callNative to intercept and modify node creation
149
+ export function patchCallNative(engine) {
150
+ if (callNativePatched) {
151
+ return
152
+ }
153
+
154
+ const bridge = global.Hippy?.bridge
155
+ if (!bridge || !bridge.callNative) {
156
+ console.error('[Web Renderer] Hippy.bridge not available!')
157
+ return
158
+ }
159
+
160
+ const originalCallNative = bridge.callNative.bind(bridge)
161
+
162
+ bridge.callNative = (moduleName, methodName, ...args) => {
163
+ // Handle ESIJKAudioPlayerModule - redirect to web audio adapter
164
+ if (moduleName === 'ESIJKAudioPlayerModule') {
165
+ const audioModule = global.__WEB_MODULES__?.['ESIJKAudioPlayerModule']
166
+ if (audioModule && typeof audioModule[methodName] === 'function') {
167
+ console.log('[Web Renderer] ESIJKAudioPlayerModule:', methodName, 'args:', args)
168
+ // Native call format: callNative(module, method, ...params)
169
+ // For play: callNative('ESIJKAudioPlayerModule', 'play', url)
170
+ // Module methods expect: method(id, ...params), but native calls don't have id
171
+ // So we pass -1 as default id
172
+ const id = -1
173
+ return audioModule[methodName](id, ...args)
174
+ }
175
+ }
176
+
177
+ if (moduleName === 'UIManagerModule') {
178
+ // Handle addEventListener - register event listener on component
179
+ if (methodName === 'addEventListener') {
180
+ const [id, eventName, handler] = args
181
+ console.log('[Web Renderer] addEventListener:', { id, eventName })
182
+
183
+ const component = findComponentById(id)
184
+ if (component && component.addEventListener) {
185
+ component.addEventListener(eventName, handler)
186
+ console.log(
187
+ '[Web Renderer] Event listener registered:',
188
+ eventName,
189
+ 'on component',
190
+ component.tagName
191
+ )
192
+ } else {
193
+ console.warn('[Web Renderer] Component not found or no addEventListener for id:', id)
194
+ }
195
+ return
196
+ }
197
+
198
+ // Handle removeEventListener
199
+ if (methodName === 'removeEventListener') {
200
+ const [id, eventName, handler] = args
201
+ console.log('[Web Renderer] removeEventListener:', { id, eventName })
202
+
203
+ const component = findComponentById(id)
204
+ if (component && component.removeEventListener) {
205
+ component.removeEventListener(eventName, handler)
206
+ }
207
+ return
208
+ }
209
+
210
+ // Handle callUIFunction
211
+ if (methodName === 'callUIFunction') {
212
+ const [nodeId, funcName, params, callback] = args
213
+ console.log('[Web Renderer] callUIFunction:', { nodeId, funcName, params })
214
+
215
+ const component = findComponentById(nodeId)
216
+ if (component && typeof component[funcName] === 'function') {
217
+ console.log('[Web Renderer] Calling', funcName, 'on component', component.tagName)
218
+ try {
219
+ const result = component[funcName](...(params || []))
220
+ if (callback && typeof callback === 'function') {
221
+ callback(result)
222
+ }
223
+ return result
224
+ } catch (e) {
225
+ console.error('[Web Renderer] Error calling', funcName, ':', e)
226
+ }
227
+ } else if (component) {
228
+ console.warn('[Web Renderer] Component', component.tagName, 'has no method:', funcName)
229
+ } else {
230
+ console.warn('[Web Renderer] Component not found for id:', nodeId)
231
+ }
232
+ return
233
+ }
234
+
235
+ if (methodName === 'createNode') {
236
+ const [rootViewId, nodes] = args
237
+
238
+ if (Array.isArray(nodes)) {
239
+ const processedNodes = nodes.map((node) => {
240
+ let nodeData = node
241
+ let props = null
242
+
243
+ if (Array.isArray(node)) {
244
+ nodeData = node[0]
245
+ props = node[1]
246
+ }
247
+
248
+ if (nodeData && typeof nodeData === 'object') {
249
+ if (!nodeData.name) {
250
+ nodeData.name = 'View'
251
+ }
252
+
253
+ // Log component creation
254
+ console.log('[Web Renderer] Creating node:', nodeData.id, 'name:', nodeData.name)
255
+
256
+ if (!rootNodeId && (nodeData.pId === 'app' || nodeData.pId === rootViewId)) {
257
+ rootNodeId = nodeData.id
258
+ console.log('[Web Renderer] ROOT NODE detected: id=' + rootNodeId)
259
+
260
+ if (nodeData.props) {
261
+ nodeData.props.style = nodeData.props.style || {}
262
+ nodeData.props.style.width = TV_WIDTH
263
+ nodeData.props.style.height = TV_HEIGHT
264
+ delete nodeData.props.style.flex
265
+ }
266
+ }
267
+ }
268
+
269
+ return Array.isArray(node) ? [nodeData, props] : nodeData
270
+ })
271
+
272
+ return originalCallNative(moduleName, methodName, rootViewId, processedNodes)
273
+ }
274
+ }
275
+
276
+ if (methodName === 'updateNode') {
277
+ const [rootViewId, nodes] = args
278
+
279
+ if (Array.isArray(nodes)) {
280
+ const processedNodes = nodes.map((node) => {
281
+ let nodeData = node
282
+ let props = null
283
+
284
+ if (Array.isArray(node)) {
285
+ nodeData = node[0]
286
+ props = node[1]
287
+ }
288
+
289
+ if (nodeData && typeof nodeData === 'object') {
290
+ if (!nodeData.name) {
291
+ nodeData.name = 'View'
292
+ }
293
+
294
+ if (rootNodeId && nodeData.id === rootNodeId) {
295
+ if (nodeData.props) {
296
+ nodeData.props.style = nodeData.props.style || {}
297
+ nodeData.props.style.width = TV_WIDTH
298
+ nodeData.props.style.height = TV_HEIGHT
299
+ delete nodeData.props.style.flex
300
+ }
301
+ }
302
+ }
303
+
304
+ return Array.isArray(node) ? [nodeData, props] : nodeData
305
+ })
306
+
307
+ return originalCallNative(moduleName, methodName, rootViewId, processedNodes)
308
+ }
309
+ }
310
+ }
311
+
312
+ // Handle ESToastModule - redirect to web toast adapter
313
+ if (moduleName === 'ESToastModule') {
314
+ if (ESToastModule && typeof ESToastModule[methodName] === 'function') {
315
+ console.log('[Web Renderer] ESToastModule:', methodName, args)
316
+ ESToastModule[methodName](...args)
317
+ return
318
+ }
319
+ }
320
+
321
+ // Handle ESNetworkSpeedModule - redirect to web network speed adapter
322
+ if (moduleName === 'ESNetworkSpeedModule') {
323
+ if (ESNetworkSpeedModule && typeof ESNetworkSpeedModule[methodName] === 'function') {
324
+ console.log('[Web Renderer] ESNetworkSpeedModule:', methodName, args)
325
+ ESNetworkSpeedModule[methodName](...args)
326
+ return
327
+ }
328
+ }
329
+
330
+ // Handle ESPluginModule - intercept plugin installation for web platform
331
+ if (moduleName === 'ESPluginModule') {
332
+ console.log(`[Web Renderer] ESPluginModule call: ${methodName}`, args)
333
+
334
+ if (methodName === 'install') {
335
+ const pluginInfo = Array.isArray(args[0]) ? args[0][0] : args[0]
336
+ const pkg = pluginInfo?.pkg
337
+ console.log(`[Web Renderer] ESPluginModule.install: ${pkg}`, pluginInfo)
338
+
339
+ // 发送 EventBus 事件(格式必须匹配 ESPlugin 的期望)
340
+ try {
341
+ const { EventBus } = require('@extscreen/es3-vue')
342
+ if (EventBus?.$emit) {
343
+ EventBus.$emit('onESPluginStateChanged', {
344
+ success: true, // 必须是 success,不是 state
345
+ pkg,
346
+ status: 0,
347
+ msg: 'Plugin installed successfully (web)',
348
+ })
349
+ console.log(`[Web Renderer] EventBus event sent for ${pkg}`)
350
+ }
351
+ } catch (e) {
352
+ console.log('[Web Renderer] EventBus error:', e.message)
353
+ }
354
+
355
+ console.log(`[Web Renderer] Plugin ${pkg} install completed (web)`)
356
+
357
+ return
358
+ }
359
+
360
+ return
361
+ }
362
+
363
+ return originalCallNative(moduleName, methodName, ...args)
364
+ }
365
+
366
+ callNativePatched = true
367
+ console.log('[Web Renderer] Patched Hippy.bridge.callNative')
368
+ }
369
+
370
+ // Patch Hippy.bridge.callNativeWithPromise for ESIJKAudioPlayerModule and RuntimeDeviceModule
371
+ export function patchCallNativeWithPromise() {
372
+ const bridge = global.Hippy?.bridge
373
+ if (!bridge || !bridge.callNativeWithPromise) {
374
+ console.log('[Web Renderer] Hippy.bridge.callNativeWithPromise not available')
375
+ return
376
+ }
377
+
378
+ const originalCallNativeWithPromise = bridge.callNativeWithPromise.bind(bridge)
379
+
380
+ bridge.callNativeWithPromise = (moduleName, methodName, ...args) => {
381
+ console.log('[Web Renderer] callNativeWithPromise:', moduleName, methodName, args)
382
+ // Handle ESIJKAudioPlayerModule - redirect to web audio adapter
383
+ if (moduleName === 'ESIJKAudioPlayerModule') {
384
+ const audioModule = global.__WEB_MODULES__?.['ESIJKAudioPlayerModule']
385
+ if (audioModule && typeof audioModule[methodName] === 'function') {
386
+ console.log('[Web Renderer] ESIJKAudioPlayerModule (Promise):', methodName, args)
387
+ // Native call format doesn't have id, pass -1 as default
388
+ const id = -1
389
+ const result = audioModule[methodName](id, ...args)
390
+ if (result instanceof Promise) {
391
+ return result
392
+ }
393
+ return Promise.resolve(result)
394
+ }
395
+ }
396
+
397
+ // Handle RuntimeDeviceModule - redirect to web device adapter
398
+ if (moduleName === 'RuntimeDeviceModule') {
399
+ if (RuntimeDeviceModule && typeof RuntimeDeviceModule[methodName] === 'function') {
400
+ console.log('[Web Renderer] RuntimeDeviceModule (Promise):', methodName, args)
401
+ const result = RuntimeDeviceModule[methodName](...args)
402
+ if (result instanceof Promise) {
403
+ return result
404
+ }
405
+ return Promise.resolve(result)
406
+ }
407
+ }
408
+
409
+ // Handle AndroidDeviceModule - redirect to web device adapter
410
+ if (moduleName === 'AndroidDeviceModule') {
411
+ if (AndroidDeviceModule && typeof AndroidDeviceModule[methodName] === 'function') {
412
+ console.log('[Web Renderer] AndroidDeviceModule (Promise):', methodName, args)
413
+ const result = AndroidDeviceModule[methodName](...args)
414
+ if (result instanceof Promise) {
415
+ return result
416
+ }
417
+ return Promise.resolve(result)
418
+ }
419
+ }
420
+
421
+ // Handle AndroidNetworkModule - redirect to web network adapter
422
+ if (moduleName === 'AndroidNetworkModule') {
423
+ if (AndroidNetworkModule && typeof AndroidNetworkModule[methodName] === 'function') {
424
+ console.log('[Web Renderer] AndroidNetworkModule (Promise):', methodName, args)
425
+ const result = AndroidNetworkModule[methodName](...args)
426
+ if (result instanceof Promise) {
427
+ return result
428
+ }
429
+ return Promise.resolve(result)
430
+ }
431
+ }
432
+
433
+ return originalCallNativeWithPromise(moduleName, methodName, ...args)
434
+ }
435
+
436
+ console.log('[Web Renderer] Patched Hippy.bridge.callNativeWithPromise')
437
+ }
438
+
439
+ // Patch Hippy.document.callUIFunction
440
+ export function patchDocumentCallUIFunction() {
441
+ const originalDocCallUIFunction = global.Hippy.document.callUIFunction.bind(global.Hippy.document)
442
+
443
+ // Retry helper for callUIFunction - handles timing issues where component not yet created
444
+ const retryCallUIFunction = (
445
+ nodeId,
446
+ funcName,
447
+ params,
448
+ callback,
449
+ retriesLeft = 10,
450
+ delay = 50
451
+ ) => {
452
+ const component = findComponentById(nodeId)
453
+ if (component && typeof component[funcName] === 'function') {
454
+ console.log(
455
+ '[Web Renderer] Retry successful - Calling',
456
+ funcName,
457
+ 'on component',
458
+ component.tagName
459
+ )
460
+ try {
461
+ const result = component[funcName](params)
462
+ if (callback && typeof callback === 'function') {
463
+ callback(result)
464
+ }
465
+ return result
466
+ } catch (e) {
467
+ console.error('[Web Renderer] Error calling', funcName, ':', e)
468
+ }
469
+ } else if (retriesLeft > 0) {
470
+ console.log(
471
+ '[Web Renderer] Component not found for id:',
472
+ nodeId,
473
+ '- retrying in',
474
+ delay,
475
+ 'ms (retries left:',
476
+ retriesLeft,
477
+ ')'
478
+ )
479
+ setTimeout(() => {
480
+ retryCallUIFunction(nodeId, funcName, params, callback, retriesLeft - 1, delay)
481
+ }, delay)
482
+ } else {
483
+ console.warn('[Web Renderer] Retry exhausted - Component still not found for id:', nodeId)
484
+ }
485
+ }
486
+
487
+ global.Hippy.document.callUIFunction = (nodeId, funcName, params, callback) => {
488
+ console.log('[Web Renderer] Hippy.document.callUIFunction:', {
489
+ nodeId,
490
+ funcName,
491
+ paramsLength: params?.length,
492
+ })
493
+
494
+ const component = findComponentById(nodeId)
495
+ if (component && typeof component[funcName] === 'function') {
496
+ console.log(
497
+ '[Web Renderer] Calling',
498
+ funcName,
499
+ 'on component',
500
+ component.tagName,
501
+ 'with',
502
+ params?.length,
503
+ 'params'
504
+ )
505
+ try {
506
+ const result = component[funcName](params)
507
+ if (callback && typeof callback === 'function') {
508
+ callback(result)
509
+ }
510
+ return result
511
+ } catch (e) {
512
+ console.error('[Web Renderer] Error calling', funcName, ':', e)
513
+ }
514
+ } else if (component) {
515
+ console.warn('[Web Renderer] Component', component.tagName, 'has no method:', funcName)
516
+ } else {
517
+ // Component not found yet - use retry mechanism
518
+ retryCallUIFunction(nodeId, funcName, params, callback)
519
+ return
520
+ }
521
+
522
+ return originalDocCallUIFunction(nodeId, funcName, params, callback)
523
+ }
524
+
525
+ console.log('[Web Renderer] Patched Hippy.document.callUIFunction')
526
+ }
527
+
528
+ // Override Hippy.device dimensions
529
+ export function overrideDeviceDimensions() {
530
+ if (typeof global.Hippy === 'undefined' || !global.Hippy.device) {
531
+ setTimeout(overrideDeviceDimensions, 10)
532
+ return
533
+ }
534
+
535
+ global.Hippy.device.window = {
536
+ width: TV_WIDTH,
537
+ height: TV_HEIGHT,
538
+ scale: 1,
539
+ fontScale: 1,
540
+ statusBarHeight: 0,
541
+ navigatorBarHeight: 0,
542
+ }
543
+
544
+ global.Hippy.device.screen = {
545
+ width: TV_WIDTH,
546
+ height: TV_HEIGHT,
547
+ scale: 1,
548
+ fontScale: 1,
549
+ statusBarHeight: 0,
550
+ navigatorBarHeight: 0,
551
+ }
552
+
553
+ global.Hippy.device.pixelRatio = 1
554
+
555
+ console.log('[Web Renderer] Overridden Hippy.device dimensions to', TV_WIDTH, 'x', TV_HEIGHT)
556
+ }
557
+
558
+ // Fix #app container style
559
+ export function fixAppContainerStyle() {
560
+ const appContainer = document.getElementById('app')
561
+ if (!appContainer) {
562
+ setTimeout(fixAppContainerStyle, 10)
563
+ return
564
+ }
565
+
566
+ appContainer.style.width = TV_WIDTH + 'px'
567
+ appContainer.style.height = TV_HEIGHT + 'px'
568
+
569
+ console.log('[Web Renderer] Fixed #app container style to', TV_WIDTH, 'x', TV_HEIGHT)
570
+
571
+ const observer = new MutationObserver(() => {
572
+ if (
573
+ appContainer.style.width !== TV_WIDTH + 'px' ||
574
+ appContainer.style.height !== TV_HEIGHT + 'px'
575
+ ) {
576
+ appContainer.style.width = TV_WIDTH + 'px'
577
+ appContainer.style.height = TV_HEIGHT + 'px'
578
+ }
579
+ })
580
+
581
+ observer.observe(appContainer, {
582
+ attributes: true,
583
+ attributeFilter: ['style'],
584
+ })
585
+ }
586
+
587
+ // Focus style property names
588
+ const FOCUS_STYLE_PROPS = [
589
+ 'focusColor',
590
+ 'focusBackgroundColor',
591
+ 'focusTextColor',
592
+ 'focusBorderColor',
593
+ 'focusBorderWidth',
594
+ 'focusBorderRadius',
595
+ 'focusOpacity',
596
+ 'focusScale',
597
+ ]
598
+
599
+ // Properties that are numeric values, not colors (should not be converted via argbToCssColor)
600
+ const NUMERIC_FOCUS_PROPS = ['focusBorderWidth', 'focusBorderRadius', 'focusOpacity', 'focusScale']
601
+
602
+ // Convert ARGB number to CSS color string
603
+ function argbToCssColor(argb) {
604
+ if (typeof argb !== 'number') return argb
605
+ const a = ((argb >> 24) & 0xff) / 255
606
+ const r = (argb >> 16) & 0xff
607
+ const g = (argb >> 8) & 0xff
608
+ const b = argb & 0xff
609
+ return `rgba(${r}, ${g}, ${b}, ${a})`
610
+ }
611
+
612
+ // Convert Android ARGB color string (#AARRGGBB) to CSS color (#RRGGBBAA or rgba())
613
+ function convertArgbColorString(color) {
614
+ if (typeof color !== 'string') return color
615
+
616
+ // Handle #AARRGGBB format (8 hex chars with #)
617
+ if (/^#[0-9A-Fa-f]{8}$/.test(color)) {
618
+ const alpha = color.substring(1, 3)
619
+ const red = color.substring(3, 5)
620
+ const green = color.substring(5, 7)
621
+ const blue = color.substring(7, 9)
622
+
623
+ // Convert hex to decimal
624
+ const a = parseInt(alpha, 16) / 255
625
+ const r = parseInt(red, 16)
626
+ const g = parseInt(green, 16)
627
+ const b = parseInt(blue, 16)
628
+
629
+ return `rgba(${r}, ${g}, ${b}, ${a})`
630
+ }
631
+
632
+ // Handle #ARGB format (4 hex chars with #)
633
+ if (/^#[0-9A-Fa-f]{4}$/.test(color)) {
634
+ const alpha = color.substring(1, 2) + color.substring(1, 2)
635
+ const red = color.substring(2, 3) + color.substring(2, 3)
636
+ const green = color.substring(3, 4) + color.substring(3, 4)
637
+ const blue = color.substring(4, 5) + color.substring(4, 5)
638
+
639
+ const a = parseInt(alpha, 16) / 255
640
+ const r = parseInt(red, 16)
641
+ const g = parseInt(green, 16)
642
+ const b = parseInt(blue, 16)
643
+
644
+ return `rgba(${r}, ${g}, ${b}, ${a})`
645
+ }
646
+
647
+ // Already in CSS format (#RRGGBB, #RGB, rgba(), etc.)
648
+ return color
649
+ }
650
+
651
+ // Gradient orientation mapping (ESGradientOrientation to CSS direction)
652
+ const GRADIENT_ORIENTATION_MAP = {
653
+ 0: 'to bottom', // TOP_BOTTOM
654
+ 1: 'to bottom left', // TR_BL
655
+ 2: 'to left', // RIGHT_LEFT
656
+ 3: 'to top left', // BR_TL
657
+ 4: 'to top', // BOTTOM_TOP
658
+ 5: 'to top right', // BL_TR
659
+ 6: 'to right', // LEFT_RIGHT
660
+ 7: 'to bottom right', // TL_BR
661
+ }
662
+
663
+ // Convert QTGradient to CSS gradient string
664
+ function gradientBackgroundToCss(gradient) {
665
+ if (!gradient || !gradient.colors || gradient.colors.length === 0) {
666
+ return null
667
+ }
668
+
669
+ // Convert colors from ARGB format to CSS format
670
+ const colors = gradient.colors.map((color) => convertArgbColorString(color))
671
+ const orientation = gradient.orientation ?? 6 // Default: LEFT_RIGHT
672
+ const type = gradient.type ?? 0 // Default: LINEAR
673
+ const cornerRadius = gradient.cornerRadius
674
+ const cornerRadii4 = gradient.cornerRadii4
675
+ const cornerRadii8 = gradient.cornerRadii8
676
+
677
+ // Build CSS gradient
678
+ let cssGradient = ''
679
+
680
+ if (type === 1) {
681
+ // Radial gradient
682
+ const gradientRadius = gradient.gradientRadius || 'farthest-corner'
683
+ cssGradient = `radial-gradient(circle ${gradientRadius}, ${colors.join(', ')})`
684
+ } else {
685
+ // Linear gradient (default)
686
+ const direction = GRADIENT_ORIENTATION_MAP[orientation] || 'to right'
687
+ cssGradient = `linear-gradient(${direction}, ${colors.join(', ')})`
688
+ }
689
+
690
+ // Build border-radius
691
+ let borderRadius = ''
692
+ if (cornerRadii8 && cornerRadii8.length === 8) {
693
+ // 8 values: [topLeftX, topLeftY, topRightX, topRightY, bottomRightX, bottomRightY, bottomLeftX, bottomLeftY]
694
+ borderRadius = `${cornerRadii8[0]}px ${cornerRadii8[2]}px ${cornerRadii8[4]}px ${cornerRadii8[6]}px / ${cornerRadii8[1]}px ${cornerRadii8[3]}px ${cornerRadii8[5]}px ${cornerRadii8[7]}px`
695
+ } else if (cornerRadii4 && cornerRadii4.length === 4) {
696
+ // 4 values: [topLeft, topRight, bottomRight, bottomLeft]
697
+ borderRadius = `${cornerRadii4[0]}px ${cornerRadii4[1]}px ${cornerRadii4[2]}px ${cornerRadii4[3]}px`
698
+ } else if (cornerRadius !== undefined && cornerRadius !== null) {
699
+ borderRadius = `${cornerRadius}px`
700
+ }
701
+
702
+ return {
703
+ background: cssGradient,
704
+ borderRadius: borderRadius,
705
+ }
706
+ }
707
+
708
+ // Extract focus styles from style object and store as data attributes
709
+ function extractFocusStyles(element, styleObj) {
710
+ if (!styleObj || typeof styleObj !== 'object') return styleObj
711
+ if (!element || !element.setAttribute) return styleObj
712
+
713
+ const result = { ...styleObj }
714
+
715
+ // Handle gradientBackground - convert to CSS gradient
716
+ const gradientBg = styleObj.gradientBackground
717
+ if (gradientBg) {
718
+ const cssGradient = gradientBackgroundToCss(gradientBg)
719
+ if (cssGradient) {
720
+ result.background = cssGradient.background
721
+ if (cssGradient.borderRadius) {
722
+ result.borderRadius = cssGradient.borderRadius
723
+ }
724
+ }
725
+ delete result.gradientBackground
726
+ }
727
+
728
+ FOCUS_STYLE_PROPS.forEach((prop) => {
729
+ const kebabProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase()
730
+ const value = styleObj[prop] || styleObj[kebabProp]
731
+
732
+ if (value !== undefined) {
733
+ // Numeric props should not be converted as colors
734
+ let cssValue
735
+ if (NUMERIC_FOCUS_PROPS.includes(prop)) {
736
+ cssValue = value
737
+ } else {
738
+ // Convert ARGB to CSS color if needed (for color props)
739
+ cssValue = typeof value === 'number' ? argbToCssColor(value) : value
740
+ }
741
+ // Store as data attribute
742
+ element.setAttribute('data-' + kebabProp, String(cssValue))
743
+ // Remove from style object (browser won't understand it)
744
+ delete result[prop]
745
+ delete result[kebabProp]
746
+ }
747
+ })
748
+
749
+ return result
750
+ }
751
+
752
+ // Patch HippyWeb.setElementStyle to extract focus styles
753
+ function patchHippyWebSetElementStyle(engine) {
754
+ const HippyWeb = engine.context?.getModuleByName?.('HippyWeb')
755
+ if (HippyWeb && HippyWeb.setElementStyle && !HippyWeb._setElementStylePatched) {
756
+ const originalSetElementStyle = HippyWeb.setElementStyle.bind(HippyWeb)
757
+ HippyWeb.setElementStyle = (element, styleObj) => {
758
+ const cleanedStyle = extractFocusStyles(element, styleObj)
759
+ originalSetElementStyle(element, cleanedStyle)
760
+ }
761
+ HippyWeb._setElementStylePatched = true
762
+ console.log('[Web Renderer] Patched HippyWeb.setElementStyle for focus styles')
763
+ }
764
+ }
765
+
766
+ function patchFocusCssVars() {
767
+ const focusKebabProps = [
768
+ 'focus-color',
769
+ 'focus-background-color',
770
+ 'focus-background',
771
+ 'focus-text-color',
772
+ 'focus-border-color',
773
+ 'focus-border-width',
774
+ 'focus-border-radius',
775
+ 'focus-opacity',
776
+ 'focus-scale',
777
+ ]
778
+
779
+ const patchRuleStyle = (style) => {
780
+ if (!style) return
781
+ focusKebabProps.forEach((prop) => {
782
+ const value = style.getPropertyValue(prop)
783
+ if (!value) return
784
+ const varName = '--' + prop
785
+ const existing = style.getPropertyValue(varName)
786
+ if (existing) return
787
+ style.setProperty(varName, value.trim())
788
+ })
789
+ }
790
+
791
+ const visitRules = (rules) => {
792
+ if (!rules) return
793
+ Array.from(rules).forEach((rule) => {
794
+ if (!rule) return
795
+ const type = rule.type
796
+ if (type === CSSRule.STYLE_RULE) {
797
+ patchRuleStyle(rule.style)
798
+ } else if (
799
+ type === CSSRule.MEDIA_RULE ||
800
+ type === CSSRule.SUPPORTS_RULE ||
801
+ type === CSSRule.DOCUMENT_RULE
802
+ ) {
803
+ visitRules(rule.cssRules)
804
+ }
805
+ })
806
+ }
807
+
808
+ Array.from(document.styleSheets || []).forEach((sheet) => {
809
+ try {
810
+ visitRules(sheet.cssRules)
811
+ } catch (e) {}
812
+ })
813
+ }
814
+
815
+ let _focusCssVarObserver = null
816
+ function ensureFocusCssVarsPatched() {
817
+ patchFocusCssVars()
818
+ if (_focusCssVarObserver) return
819
+ _focusCssVarObserver = new MutationObserver(() => patchFocusCssVars())
820
+ _focusCssVarObserver.observe(document.head, { childList: true, subtree: true })
821
+ }
822
+
823
+ // Patch UIManagerModule
824
+ export function patchUIManager(engine) {
825
+ const uiManager = engine.context?.getModuleByName?.('UIManagerModule')
826
+ if (uiManager && !uiManager._addEventListenerPatched) {
827
+ const originalAddEventListener = uiManager.addEventListener.bind(uiManager)
828
+ uiManager.addEventListener = (id, eventName, callback) => {
829
+ const view = uiManager.findViewById(id)
830
+ if (view && view.addEventListener) {
831
+ view.addEventListener(eventName, callback)
832
+ }
833
+ }
834
+
835
+ const originalDefaultUpdateViewProps = uiManager.defaultUpdateViewProps.bind(uiManager)
836
+ uiManager.defaultUpdateViewProps = (view, props) => {
837
+ if (!view.dom) {
838
+ console.warn('[UIManagerModule] defaultUpdateViewProps: view.dom is null, skipping')
839
+ return
840
+ }
841
+
842
+ if (props?.style) {
843
+ // If this is ESPageRootView or ESPageRouterView, intercept and force default background
844
+ if (
845
+ (view._originalComponentName === 'ESPageRootView' ||
846
+ view._originalComponentName === 'ESPageRouterView') &&
847
+ (!props.style.backgroundColor || props.style.backgroundColor === 'transparent') &&
848
+ !props.isDialogMode
849
+ ) {
850
+ props.style.backgroundColor = '#000000'
851
+ }
852
+
853
+ props.style = extractFocusStyles(view.dom, props.style)
854
+ }
855
+
856
+ const normalizeClass = (value) => {
857
+ if (!value) return ''
858
+ if (typeof value === 'string') return value
859
+ if (Array.isArray(value)) return value.map(normalizeClass).filter(Boolean).join(' ')
860
+ if (typeof value === 'object')
861
+ return Object.keys(value)
862
+ .filter((k) => value[k])
863
+ .join(' ')
864
+ return String(value)
865
+ }
866
+
867
+ const applyClass = (element, value) => {
868
+ const cls = normalizeClass(value)
869
+ if (!cls) return
870
+ cls
871
+ .split(/\s+/)
872
+ .filter(Boolean)
873
+ .forEach((c) => element.classList.add(c))
874
+ }
875
+
876
+ const applyFocusProps = (element, p) => {
877
+ if (!element || !element.setAttribute || !p) return
878
+ FOCUS_STYLE_PROPS.forEach((prop) => {
879
+ const kebabProp = prop.replace(/([A-Z])/g, '-$1').toLowerCase()
880
+ const value = p[prop] ?? p[kebabProp]
881
+ if (value === undefined || value === null) return
882
+ // Numeric props should not be converted as colors
883
+ const cssValue = NUMERIC_FOCUS_PROPS.includes(prop)
884
+ ? value
885
+ : typeof value === 'number'
886
+ ? argbToCssColor(value)
887
+ : value
888
+ element.setAttribute('data-' + kebabProp, String(cssValue))
889
+ })
890
+ }
891
+
892
+ applyClass(view.dom, props?.className || props?.class)
893
+ applyFocusProps(view.dom, props)
894
+ const parent = uiManager.findViewById(view.pId)
895
+ if (!parent || !parent.dom) {
896
+ console.log(
897
+ '[UIManagerModule] defaultUpdateViewProps: parent or parent.dom is null, using safe update'
898
+ )
899
+ const keys = Object.keys(props)
900
+ if (props.style) {
901
+ const HippyWeb = engine.context.getModuleByName('HippyWeb')
902
+ if (HippyWeb?.setElementStyle) {
903
+ HippyWeb.setElementStyle(view.dom, props.style)
904
+ } else {
905
+ Object.assign(view.dom.style, props.style)
906
+ }
907
+ }
908
+ return
909
+ }
910
+ originalDefaultUpdateViewProps(view, props)
911
+ applyClass(view.dom, props?.className || props?.class)
912
+ applyFocusProps(view.dom, props)
913
+
914
+ // Apply gradientBackground from props (not in style)
915
+ if (props?.gradientBackground) {
916
+ const cssGradient = gradientBackgroundToCss(props.gradientBackground)
917
+ if (cssGradient) {
918
+ view.dom.style.background = cssGradient.background
919
+ if (cssGradient.borderRadius) {
920
+ view.dom.style.borderRadius = cssGradient.borderRadius
921
+ }
922
+ }
923
+ }
924
+ }
925
+
926
+ uiManager._addEventListenerPatched = true
927
+ console.log('[Web Renderer] Patched UIManagerModule methods for debugging and safety')
928
+ return true
929
+ }
930
+ return false
931
+ }
932
+
933
+ // Apply all patches
934
+ export function applyAllPatches(engine) {
935
+ setWebEngine(engine)
936
+ patchHippyCallNatives(engine)
937
+ patchCallNative(engine)
938
+ patchCallNativeWithPromise()
939
+ patchDocumentCallUIFunction()
940
+ overrideDeviceDimensions()
941
+ fixAppContainerStyle()
942
+
943
+ // Patch HippyWeb.setElementStyle for focus styles
944
+ patchHippyWebSetElementStyle(engine)
945
+ ensureFocusCssVarsPatched()
946
+
947
+ // Try to patch UIManagerModule
948
+ if (!patchUIManager(engine)) {
949
+ const tryPatch = () => {
950
+ if (patchUIManager(engine)) {
951
+ console.log('[Web Renderer] UIManagerModule patched via requestAnimationFrame')
952
+ } else {
953
+ requestAnimationFrame(tryPatch)
954
+ }
955
+ }
956
+ requestAnimationFrame(tryPatch)
957
+ }
958
+ }