@quicktvui/web-renderer 1.0.7 → 1.0.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quicktvui/web-renderer",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "Web renderer for QuickTVUI - provides web browser rendering support",
5
5
  "author": "QuickTVUI Team",
6
6
  "license": "Apache-2.0",
@@ -1,16 +1,25 @@
1
1
  /**
2
2
  * QtBaseComponent - Base class for all QuickTV UI components
3
- * Provides common property handling (visible, visibility, showOnState, etc.)
3
+ * Provides common property handling (visible, visibility, etc.)
4
4
  */
5
5
  import { HippyWebView } from '@hippy/web-renderer'
6
6
 
7
+ /**
8
+ * Visibility enum values
9
+ * @typedef {'visible' | 'invisible' | 'gone'} QTVisibility
10
+ */
11
+
7
12
  export class QtBaseComponent extends HippyWebView {
8
13
  constructor(context, id, pId) {
9
14
  super(context, id, pId)
15
+ // Store original display value for visibility toggle
10
16
  this._originalDisplay = undefined
11
- this._showOnState = null
12
17
  }
13
18
 
19
+ /**
20
+ * Override updateProperty to handle common properties first
21
+ * Subclasses should call super.updateProperty(key, value) for unhandled properties
22
+ */
14
23
  updateProperty(key, value) {
15
24
  if (key === 'type' && this.dom) {
16
25
  if (value !== null && value !== undefined && value !== '') {
@@ -28,6 +37,12 @@ export class QtBaseComponent extends HippyWebView {
28
37
  super.updateProperty(key, value)
29
38
  }
30
39
 
40
+ /**
41
+ * Handle common properties
42
+ * @param {string} key - Property name
43
+ * @param {*} value - Property value
44
+ * @returns {boolean} - True if handled
45
+ */
31
46
  _handleCommonProperty(key, value) {
32
47
  switch (key) {
33
48
  case 'visible':
@@ -39,6 +54,7 @@ export class QtBaseComponent extends HippyWebView {
39
54
  return true
40
55
 
41
56
  case 'autofocus':
57
+ // 将 autofocus 属性设置到 DOM 元素上,供 TVFocusManager 识别
42
58
  if (value === true || value === 'true' || value === 1 || value === '1') {
43
59
  this.dom.setAttribute('autofocus', 'true')
44
60
  } else {
@@ -47,6 +63,7 @@ export class QtBaseComponent extends HippyWebView {
47
63
  return true
48
64
 
49
65
  case 'name':
66
+ // 存储 name 属性到组件实例和 DOM 上,用于子组件查找
50
67
  this.props = this.props || {}
51
68
  this.props.name = value
52
69
  if (value) {
@@ -57,7 +74,18 @@ export class QtBaseComponent extends HippyWebView {
57
74
  return true
58
75
 
59
76
  case 'showOnState':
60
- this._setShowOnState(value)
77
+ // showOnState 属性:根据状态控制元素可见性
78
+ // 支持 'normal' | 'focused' | 'selected' | 'disabled' 或数组形式
79
+ if (value !== null && value !== undefined) {
80
+ if (Array.isArray(value)) {
81
+ this.dom.setAttribute('showOnState', JSON.stringify(value))
82
+ } else {
83
+ this.dom.setAttribute('showOnState', String(value))
84
+ }
85
+ } else {
86
+ this.dom.removeAttribute('showOnState')
87
+ this.dom.removeAttribute('showonstate')
88
+ }
61
89
  return true
62
90
 
63
91
  default:
@@ -65,17 +93,34 @@ export class QtBaseComponent extends HippyWebView {
65
93
  }
66
94
  }
67
95
 
96
+ /**
97
+ * Set component visibility via boolean
98
+ * @param {boolean} visible - true to show, false to hide
99
+ */
68
100
  _setVisible(visible) {
69
101
  if (visible === true) {
102
+ // Restore original display value if saved
70
103
  if (this._originalDisplay !== undefined) {
71
104
  this.dom.style.display = this._originalDisplay
72
105
  } else {
106
+ // In Hippy, all elements default to flex layout
107
+ // Check if the element has flex properties and set display: flex accordingly
73
108
  const style = this.dom.style
109
+ const hasFlexProperties =
110
+ style.flexDirection ||
111
+ style.justifyContent ||
112
+ style.alignItems ||
113
+ style.flexWrap ||
114
+ style.flexGrow ||
115
+ style.flexShrink
116
+
117
+ // Default to 'flex' for Hippy compatibility, unless display is already set
74
118
  if (!style.display || style.display === 'none') {
75
- style.display = 'flex'
119
+ style.display = hasFlexProperties ? 'flex' : 'flex'
76
120
  }
77
121
  }
78
122
  } else {
123
+ // Save current display value before hiding
79
124
  if (this._originalDisplay === undefined) {
80
125
  const currentDisplay = this.dom.style.display
81
126
  if (currentDisplay && currentDisplay !== 'none') {
@@ -86,12 +131,19 @@ export class QtBaseComponent extends HippyWebView {
86
131
  }
87
132
  }
88
133
 
134
+ /**
135
+ * Set component visibility via enum
136
+ * @param {QTVisibility} visibility - 'visible' | 'invisible' | 'gone'
137
+ */
89
138
  _setVisibility(visibility) {
90
139
  switch (visibility) {
91
140
  case 'visible':
141
+ // Restore display and ensure visibility
92
142
  if (this._originalDisplay !== undefined) {
93
143
  this.dom.style.display = this._originalDisplay
94
144
  } else {
145
+ // In Hippy, all elements default to flex layout
146
+ // Default to 'flex' for Hippy compatibility
95
147
  const style = this.dom.style
96
148
  if (!style.display || style.display === 'none') {
97
149
  style.display = 'flex'
@@ -101,10 +153,12 @@ export class QtBaseComponent extends HippyWebView {
101
153
  break
102
154
 
103
155
  case 'invisible':
156
+ // Hidden but takes space
104
157
  this.dom.style.visibility = 'hidden'
105
158
  break
106
159
 
107
160
  case 'gone':
161
+ // Completely hidden, doesn't take space
108
162
  if (this._originalDisplay === undefined) {
109
163
  const currentDisplay = this.dom.style.display
110
164
  if (currentDisplay && currentDisplay !== 'none') {
@@ -119,91 +173,19 @@ export class QtBaseComponent extends HippyWebView {
119
173
  }
120
174
  }
121
175
 
122
- _setShowOnState(value) {
123
- if (value === null || value === undefined) {
124
- this._showOnState = null
125
- this.dom.removeAttribute('showOnState')
126
- this.dom.style.display = ''
127
- return
128
- }
129
-
130
- this._showOnState = value
131
-
132
- if (Array.isArray(value)) {
133
- this.dom.setAttribute('showOnState', JSON.stringify(value))
134
- } else {
135
- this.dom.setAttribute('showOnState', String(value))
136
- }
137
-
138
- this._initShowOnStateVisibility()
139
- }
140
-
141
- _initShowOnStateVisibility() {
142
- if (!this._showOnState) return
143
-
144
- const focusedParent = this.dom.closest('.focused')
145
- const selectedParent = this.dom.closest('[selected="true"], [selected=""]')
146
-
147
- let currentState = 'normal'
148
- if (focusedParent) {
149
- currentState = 'focused'
150
- } else if (selectedParent) {
151
- currentState = 'selected'
152
- }
153
-
154
- this._applyShowOnState(currentState)
155
- }
156
-
157
- _applyShowOnState(state) {
158
- if (!this._showOnState) return
159
-
160
- const states = this._parseShowOnState(this._showOnState)
161
- const shouldShow = states.includes(state.toLowerCase())
162
-
163
- if (shouldShow) {
164
- if (this._originalDisplay !== undefined) {
165
- this.dom.style.display = this._originalDisplay
166
- } else {
167
- this.dom.style.display = ''
168
- }
169
- } else {
170
- if (this._originalDisplay === undefined) {
171
- const currentDisplay = this.dom.style.display
172
- if (currentDisplay && currentDisplay !== 'none') {
173
- this._originalDisplay = currentDisplay
174
- }
175
- }
176
- this.dom.style.display = 'none'
177
- }
178
- }
179
-
180
- _parseShowOnState(value) {
181
- if (Array.isArray(value)) {
182
- return value.map((s) => String(s).toLowerCase())
183
- }
184
-
185
- if (typeof value === 'string') {
186
- try {
187
- const parsed = JSON.parse(value)
188
- if (Array.isArray(parsed)) {
189
- return parsed.map((s) => String(s).toLowerCase())
190
- }
191
- } catch (e) {}
192
- return [value.toLowerCase()]
193
- }
194
-
195
- return []
196
- }
197
-
176
+ /**
177
+ * Public method to set visibility via boolean
178
+ * @param {boolean} visible - true to show, false to hide
179
+ */
198
180
  setVisible(visible) {
199
181
  this._setVisible(visible)
200
182
  }
201
183
 
184
+ /**
185
+ * Public method to set visibility via enum
186
+ * @param {QTVisibility} visibility - 'visible' | 'invisible' | 'gone'
187
+ */
202
188
  setVisibility(visibility) {
203
189
  this._setVisibility(visibility)
204
190
  }
205
-
206
- setShowOnState(value) {
207
- this._setShowOnState(value)
208
- }
209
191
  }
@@ -973,12 +973,66 @@ export class QtFastListView extends QtBaseComponent {
973
973
  }
974
974
  }
975
975
 
976
- // Request focus on item
976
+ // Request focus on item at position (called by qt-grid-view setItemFocused)
977
+ requestFocus(position) {
978
+ console.log('[QtFastListView] requestFocus called with position:', position)
979
+ const items = this._itemContainer.children
980
+ if (position < 0 || position >= items.length) {
981
+ console.warn(
982
+ '[QtFastListView] requestFocus: invalid position',
983
+ position,
984
+ 'items count:',
985
+ items.length
986
+ )
987
+ return
988
+ }
989
+ const item = items[position]
990
+ if (!item) {
991
+ console.warn('[QtFastListView] requestFocus: no item at position', position)
992
+ return
993
+ }
994
+
995
+ // 找到 item 内部可聚焦的元素
996
+ const focusable = item.querySelector('[focusable="true"], [focusable="1"], [focusable]') || item
997
+ const focusManager = global.__TV_FOCUS_MANAGER__
998
+ if (focusManager) {
999
+ focusManager.updateFocusableElements()
1000
+ focusManager._doFocusElement(focusable)
1001
+ } else {
1002
+ focusable.focus()
1003
+ }
1004
+ console.log('[QtFastListView] requestFocus: focused item at position', position)
1005
+ }
1006
+
1007
+ // Request focus on item (called by tv-list requestFocus)
977
1008
  requestChildFocus(position) {
978
- const item = this._itemContainer.children[position]
979
- if (item) {
980
- item.focus()
1009
+ console.log('[QtFastListView] requestChildFocus called with position:', position)
1010
+ const items = this._itemContainer.children
1011
+ if (position < 0 || position >= items.length) {
1012
+ console.warn(
1013
+ '[QtFastListView] requestChildFocus: invalid position',
1014
+ position,
1015
+ 'items count:',
1016
+ items.length
1017
+ )
1018
+ return
1019
+ }
1020
+ const item = items[position]
1021
+ if (!item) {
1022
+ console.warn('[QtFastListView] requestChildFocus: no item at position', position)
1023
+ return
1024
+ }
1025
+
1026
+ // 找到 item 内部可聚焦的元素
1027
+ const focusable = item.querySelector('[focusable="true"], [focusable="1"], [focusable]') || item
1028
+ const focusManager = global.__TV_FOCUS_MANAGER__
1029
+ if (focusManager) {
1030
+ focusManager.updateFocusableElements()
1031
+ focusManager._doFocusElement(focusable)
1032
+ } else {
1033
+ focusable.focus()
981
1034
  }
1035
+ console.log('[QtFastListView] requestChildFocus: focused item at position', position)
982
1036
  }
983
1037
 
984
1038
  // Set span count for grid layout
@@ -1,6 +1,7 @@
1
1
  // QtImage component for image handling
2
2
  import { QtBaseComponent } from './QtBaseComponent'
3
3
  import { registerComponent } from '../core/componentRegistry'
4
+ import { normalizeHpfileAssetUrl } from '../core/styleBackground'
4
5
 
5
6
  export class QtImage extends QtBaseComponent {
6
7
  constructor(context, id, pId) {
@@ -113,6 +114,8 @@ export class QtImage extends QtBaseComponent {
113
114
  }
114
115
  }
115
116
 
117
+ srcValue = normalizeHpfileAssetUrl(srcValue)
118
+
116
119
  // Store for later use (e.g., repeat mode)
117
120
  this._currentSrc = srcValue
118
121
 
@@ -1,6 +1,7 @@
1
1
  // QtTransitionImage - 支持过渡效果的图片组件
2
2
  import { QtBaseComponent } from './QtBaseComponent'
3
3
  import { registerComponent } from '../core/componentRegistry'
4
+ import { normalizeHpfileAssetUrl } from '../core/styleBackground'
4
5
 
5
6
  export class QtTransitionImage extends QtBaseComponent {
6
7
  constructor(context, id, pId) {
@@ -58,6 +59,8 @@ export class QtTransitionImage extends QtBaseComponent {
58
59
  srcValue = value.uri
59
60
  }
60
61
 
62
+ srcValue = normalizeHpfileAssetUrl(srcValue)
63
+
61
64
  // 如果是同一张图片,不做处理
62
65
  if (srcValue === this._currentSrc) return
63
66
 
@@ -1,6 +1,7 @@
1
1
  // Base QtView component that maps to div
2
2
  import { QtBaseComponent } from './QtBaseComponent'
3
3
  import { registerComponentBySid, unregisterComponentBySid } from '../core/componentRegistry'
4
+ import { normalizeBackgroundStyleValue, normalizeHpfileAssetUrl } from '../core/styleBackground'
4
5
 
5
6
  // Global registry for DOM-to-Component mapping
6
7
  window.__HIPPY_COMPONENT_REGISTRY__ = window.__HIPPY_COMPONENT_REGISTRY__ || new Map()
@@ -138,7 +139,6 @@ export class QtView extends QtBaseComponent {
138
139
  'duplicateParentState',
139
140
  'blockFocusDirections',
140
141
  'blockfocusdirections',
141
- 'showOnState',
142
142
  ]
143
143
 
144
144
  // Focus style attributes (for duplicateParentState)
@@ -259,6 +259,13 @@ export class QtView extends QtBaseComponent {
259
259
  Object.keys(styleObj).forEach((key) => {
260
260
  let value = styleObj[key]
261
261
 
262
+ if (key === 'backgroundImage' || key === 'background') {
263
+ const normalizedBackground = normalizeBackgroundStyleValue(key, value)
264
+ if (normalizedBackground !== value) {
265
+ value = normalizedBackground
266
+ }
267
+ }
268
+
262
269
  // Convert numeric values to pixels for appropriate properties
263
270
  if (typeof value === 'number') {
264
271
  const needsPx = [
@@ -581,6 +588,24 @@ export class QtView extends QtBaseComponent {
581
588
  console.log('[QtView] setBackGroundColor:', cssColor)
582
589
  }
583
590
 
591
+ /**
592
+ * Set the background image of the element
593
+ * @param {string} src - image URL
594
+ */
595
+ setBackgroundImage(src) {
596
+ console.log('[QtView] setBackgroundImage called with:', src, 'type:', typeof src)
597
+
598
+ if (!src) {
599
+ this.dom.style.backgroundImage = ''
600
+ return
601
+ }
602
+
603
+ let srcValue = normalizeHpfileAssetUrl(src)
604
+
605
+ this.dom.style.backgroundImage = `url(${srcValue})`
606
+ console.log('[QtView] Set backgroundImage to:', srcValue)
607
+ }
608
+
584
609
  /**
585
610
  * Set the size of the element
586
611
  * @param {number} width - width in pixels
@@ -223,6 +223,31 @@ export class TVFocusManager {
223
223
  return `[id="${id}"]`
224
224
  }
225
225
 
226
+ // Try data-position or data-index (for FastListView items)
227
+ const dataPosition = element.getAttribute('data-position')
228
+ const dataIndex = element.getAttribute('data-index')
229
+ if (dataPosition !== null) {
230
+ // 找到 FastListView 容器,生成完整选择器
231
+ const container = this._findFastListViewContainer(element)
232
+ if (container) {
233
+ const containerId = container.id
234
+ if (containerId) {
235
+ return `[id="${containerId}"] [data-position="${dataPosition}"]`
236
+ }
237
+ }
238
+ return `[data-position="${dataPosition}"]`
239
+ }
240
+ if (dataIndex !== null) {
241
+ const container = this._findFastListViewContainer(element)
242
+ if (container) {
243
+ const containerId = container.id
244
+ if (containerId) {
245
+ return `[id="${containerId}"] [data-index="${dataIndex}"]`
246
+ }
247
+ }
248
+ return `[data-index="${dataIndex}"]`
249
+ }
250
+
226
251
  // Generate path-based selector
227
252
  const path = []
228
253
  let current = element
@@ -237,6 +262,20 @@ export class TVFocusManager {
237
262
  break
238
263
  }
239
264
 
265
+ // 尝试使用 data-position 或 data-index
266
+ const pos = current.getAttribute('data-position')
267
+ const idx = current.getAttribute('data-index')
268
+ if (pos !== null) {
269
+ selector = `[data-position="${pos}"]`
270
+ path.unshift(selector)
271
+ break
272
+ }
273
+ if (idx !== null) {
274
+ selector = `[data-index="${idx}"]`
275
+ path.unshift(selector)
276
+ break
277
+ }
278
+
240
279
  if (current.className && typeof current.className === 'string') {
241
280
  const classes = current.className.split(' ').filter((c) => c && !c.includes(':'))
242
281
  if (classes.length > 0) {
@@ -261,6 +300,22 @@ export class TVFocusManager {
261
300
  return path.join(' > ')
262
301
  }
263
302
 
303
+ /**
304
+ * 找到元素所在的 FastListView 容器
305
+ */
306
+ _findFastListViewContainer(element) {
307
+ if (!element) return null
308
+ let current = element
309
+ while (current && current !== document.body) {
310
+ const componentName = current.getAttribute('data-component-name')
311
+ if (componentName === 'FastListView') {
312
+ return current
313
+ }
314
+ current = current.parentElement
315
+ }
316
+ return null
317
+ }
318
+
264
319
  resetForNewPage() {
265
320
  // console.log('[TVFocus] Resetting focus for new page')
266
321
  this.clearFocus()
@@ -537,6 +592,33 @@ export class TVFocusManager {
537
592
  handleKeyDown(e) {
538
593
  this._dispatchPageKeyDown(e)
539
594
 
595
+ // 检查当前焦点元素是否在当前页面中有效
596
+ if (this.focusedElement) {
597
+ const currentPage = this._getCurrentPage()
598
+ const isInCurrentPage = currentPage && currentPage.contains(this.focusedElement)
599
+ const isInDOM = document.body.contains(this.focusedElement)
600
+ if (!isInCurrentPage || !isInDOM) {
601
+ console.log('[TVFocusManager] focusedElement 不在当前页面,清除焦点')
602
+ this.clearFocus()
603
+ }
604
+ }
605
+
606
+ // 当页面没有焦点时,按任意键(Escape除外)都聚焦到第一个可聚焦元素
607
+ if (!this.focusedElement && e.key !== 'Escape') {
608
+ // 先尝试从当前页面的 PageRootView 恢复焦点状态
609
+ if (this._restoreFocusFromPageRootView()) {
610
+ e.preventDefault()
611
+ return
612
+ }
613
+ // 没有保存的焦点状态,聚焦第一个元素
614
+ this.updateFocusableElements()
615
+ if (this.focusableElements.length > 0) {
616
+ this._doFocusElement(this.focusableElements[0])
617
+ e.preventDefault()
618
+ return
619
+ }
620
+ }
621
+
540
622
  if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Enter', 'Escape'].includes(e.key)) {
541
623
  return
542
624
  }
@@ -1179,6 +1261,43 @@ export class TVFocusManager {
1179
1261
  return [String(attrValue).toLowerCase()]
1180
1262
  }
1181
1263
 
1264
+ _getShowOnStateAttr(element) {
1265
+ if (!element || !element.getAttribute) return null
1266
+ return element.getAttribute('showOnState') || element.getAttribute('showonstate')
1267
+ }
1268
+
1269
+ _getShowOnStateElements(container, includeSelf = false) {
1270
+ if (!container || !container.querySelectorAll) return []
1271
+
1272
+ const elements = Array.from(container.querySelectorAll('[showOnState], [showonstate]'))
1273
+ if (includeSelf && container.matches?.('[showOnState], [showonstate]')) {
1274
+ elements.unshift(container)
1275
+ }
1276
+
1277
+ return elements
1278
+ }
1279
+
1280
+ _resolveShowOnStateState(element) {
1281
+ if (!element) return 'normal'
1282
+
1283
+ if (
1284
+ this.focusedElement &&
1285
+ (element === this.focusedElement || this.focusedElement.contains(element))
1286
+ ) {
1287
+ return 'focused'
1288
+ }
1289
+
1290
+ if (element.closest('.focused')) {
1291
+ return 'focused'
1292
+ }
1293
+
1294
+ if (element.closest('[selected="true"], [selected=""]')) {
1295
+ return 'selected'
1296
+ }
1297
+
1298
+ return 'normal'
1299
+ }
1300
+
1182
1301
  /**
1183
1302
  * 检查元素是否应该在指定状态显示
1184
1303
  * @param {HTMLElement} element - 要检查的元素
@@ -1186,7 +1305,7 @@ export class TVFocusManager {
1186
1305
  * @returns {boolean|null} - 是否应该显示,null 表示没有 showOnState 属性
1187
1306
  */
1188
1307
  _shouldShowOnState(element, state) {
1189
- const showOnStateAttr = element.getAttribute('showOnState')
1308
+ const showOnStateAttr = this._getShowOnStateAttr(element)
1190
1309
  if (!showOnStateAttr) return null
1191
1310
 
1192
1311
  const states = this._parseShowOnState(showOnStateAttr)
@@ -1213,7 +1332,7 @@ export class TVFocusManager {
1213
1332
  }
1214
1333
 
1215
1334
  // 更新所有带 showOnState 属性的子元素
1216
- const showOnStateChildren = element.querySelectorAll('[showOnState]')
1335
+ const showOnStateChildren = this._getShowOnStateElements(element)
1217
1336
  showOnStateChildren.forEach((child) => {
1218
1337
  const childShouldShow = this._shouldShowOnState(child, state)
1219
1338
  if (childShouldShow !== null) {
@@ -1232,22 +1351,10 @@ export class TVFocusManager {
1232
1351
  * 默认设置为 'normal' 状态
1233
1352
  */
1234
1353
  _initShowOnStateVisibility() {
1235
- const showOnStateElements = document.querySelectorAll('[showOnState]')
1354
+ const showOnStateElements = document.querySelectorAll('[showOnState], [showonstate]')
1236
1355
  showOnStateElements.forEach((element) => {
1237
- // 检查元素是否有 duplicateParentState 属性
1238
- const hasDuplicateParentState =
1239
- element.hasAttribute('duplicateParentState') || element.hasAttribute('duplicateparentstate')
1240
-
1241
- // 检查元素是否在已获得焦点的元素内
1242
- const focusedParent = element.closest('.focused')
1243
-
1244
- if (focusedParent) {
1245
- // 如果在已获得焦点的元素内,使用 focused 状态
1246
- this._updateShowOnStateVisibilityForElement(element, 'focused')
1247
- } else {
1248
- // 否则使用 normal 状态
1249
- this._updateShowOnStateVisibilityForElement(element, 'normal')
1250
- }
1356
+ const state = this._resolveShowOnStateState(element)
1357
+ this._updateShowOnStateVisibilityForElement(element, state)
1251
1358
  })
1252
1359
  }
1253
1360
 
@@ -1276,8 +1383,7 @@ export class TVFocusManager {
1276
1383
  initShowOnStateForElement(container) {
1277
1384
  if (!container) return
1278
1385
 
1279
- // 查找容器内所有带 showOnState 属性的元素
1280
- const showOnStateElements = container.querySelectorAll('[showOnState]')
1386
+ const showOnStateElements = this._getShowOnStateElements(container, true)
1281
1387
  console.log(
1282
1388
  '[TVFocusManager] initShowOnStateForElement found',
1283
1389
  showOnStateElements.length,
@@ -1285,17 +1391,8 @@ export class TVFocusManager {
1285
1391
  )
1286
1392
 
1287
1393
  showOnStateElements.forEach((element) => {
1288
- // 检查元素是否在已获得焦点的元素内
1289
- const focusedParent = element.closest('.focused')
1290
- const selectedParent = element.closest('[selected="true"], [selected=""]')
1291
-
1292
- if (focusedParent) {
1293
- this._updateShowOnStateVisibilityForElement(element, 'focused')
1294
- } else if (selectedParent) {
1295
- this._updateShowOnStateVisibilityForElement(element, 'selected')
1296
- } else {
1297
- this._updateShowOnStateVisibilityForElement(element, 'normal')
1298
- }
1394
+ const state = this._resolveShowOnStateState(element)
1395
+ this._updateShowOnStateVisibilityForElement(element, state)
1299
1396
  })
1300
1397
  }
1301
1398
 
@@ -1431,6 +1528,199 @@ export class TVFocusManager {
1431
1528
  // console.log('[TVFocus] Focused element', element)
1432
1529
  }
1433
1530
 
1531
+ /**
1532
+ * 保存当前页面的焦点状态(供外部调用,如 patches.js)
1533
+ * 在创建新页面前保存当前页面焦点
1534
+ */
1535
+ saveCurrentPageFocus() {
1536
+ if (!this.focusedElement) {
1537
+ console.log('[TVFocusManager] saveCurrentPageFocus: 当前没有焦点元素')
1538
+ return
1539
+ }
1540
+ const pageRootView = this._findPageRootView(this.focusedElement)
1541
+ if (!pageRootView) {
1542
+ console.log('[TVFocusManager] saveCurrentPageFocus: 未找到 PageRootView')
1543
+ return
1544
+ }
1545
+
1546
+ // 检查焦点是否在 FastListView 内
1547
+ const fastListInfo = this._getFastListViewFocusInfo(this.focusedElement)
1548
+ if (fastListInfo) {
1549
+ // 存储 FastListView 焦点信息
1550
+ pageRootView.setAttribute('data-last-fastlist-id', fastListInfo.containerId)
1551
+ pageRootView.setAttribute('data-last-fastlist-position', fastListInfo.position)
1552
+ console.log(
1553
+ '[TVFocusManager] 保存 FastListView 焦点,容器ID:',
1554
+ fastListInfo.containerId,
1555
+ '位置:',
1556
+ fastListInfo.position
1557
+ )
1558
+ } else {
1559
+ // 清除之前的 FastListView 信息
1560
+ pageRootView.removeAttribute('data-last-fastlist-id')
1561
+ pageRootView.removeAttribute('data-last-fastlist-position')
1562
+ }
1563
+
1564
+ const selector = this._generateElementSelector(this.focusedElement)
1565
+ pageRootView.setAttribute('data-last-focus-selector', selector)
1566
+ console.log('[TVFocusManager] 保存当前页面焦点到 PageRootView,选择器:', selector)
1567
+ }
1568
+
1569
+ /**
1570
+ * 获取 FastListView 内的焦点信息
1571
+ * @returns {{ containerId: string, position: number } | null}
1572
+ */
1573
+ _getFastListViewFocusInfo(element) {
1574
+ if (!element) return null
1575
+
1576
+ // 查找元素或其父元素的 data-position 或 data-index
1577
+ let current = element
1578
+ let position = null
1579
+ let container = null
1580
+
1581
+ while (current && current !== document.body) {
1582
+ const pos = current.getAttribute('data-position')
1583
+ const idx = current.getAttribute('data-index')
1584
+ if (pos !== null) {
1585
+ position = parseInt(pos)
1586
+ } else if (idx !== null) {
1587
+ position = parseInt(idx)
1588
+ }
1589
+
1590
+ const componentName = current.getAttribute('data-component-name')
1591
+ if (componentName === 'FastListView') {
1592
+ container = current
1593
+ break
1594
+ }
1595
+ current = current.parentElement
1596
+ }
1597
+
1598
+ if (container && position !== null) {
1599
+ return {
1600
+ containerId: container.id,
1601
+ position: position,
1602
+ }
1603
+ }
1604
+ return null
1605
+ }
1606
+
1607
+ /**
1608
+ * 从 ESPageRootView DOM 恢复焦点状态
1609
+ */
1610
+ _restoreFocusFromPageRootView() {
1611
+ const currentPage = this._getCurrentPage()
1612
+ if (!currentPage) return false
1613
+
1614
+ // 优先检查 FastListView 焦点信息
1615
+ const fastListId = currentPage.getAttribute('data-last-fastlist-id')
1616
+ const fastListPosition = currentPage.getAttribute('data-last-fastlist-position')
1617
+ if (fastListId && fastListPosition !== null) {
1618
+ console.log(
1619
+ '[TVFocusManager] 检测到 FastListView 焦点信息,容器ID:',
1620
+ fastListId,
1621
+ '位置:',
1622
+ fastListPosition
1623
+ )
1624
+ const container = document.getElementById(fastListId)
1625
+ if (container && container.__fastListViewInstance) {
1626
+ const fastList = container.__fastListViewInstance
1627
+ const position = parseInt(fastListPosition)
1628
+ // 延迟调用,等待元素渲染完成
1629
+ setTimeout(() => {
1630
+ fastList.requestChildFocus(position)
1631
+ }, 100)
1632
+ return true
1633
+ }
1634
+ }
1635
+
1636
+ const selector = currentPage.getAttribute('data-last-focus-selector')
1637
+ if (!selector) {
1638
+ console.log('[TVFocusManager] PageRootView 没有保存的焦点状态')
1639
+ return false
1640
+ }
1641
+
1642
+ console.log('[TVFocusManager] 从 PageRootView 恢复焦点,选择器:', selector)
1643
+ this.updateFocusableElements()
1644
+ const element = document.querySelector(selector)
1645
+ if (element && this.focusableElements.includes(element)) {
1646
+ this._doFocusElement(element)
1647
+ return true
1648
+ }
1649
+
1650
+ // 尝试解析 FastListView 相关的选择器
1651
+ // 格式: [id="containerId"] [data-position="X"] 或 [data-position="X"]
1652
+ const positionMatch = selector.match(/\[data-position="(\d+)"\]/)
1653
+ const indexMatch = selector.match(/\[data-index="(\d+)"\]/)
1654
+ const containerMatch = selector.match(/\[id="([^"]+)"\]/)
1655
+
1656
+ if ((positionMatch || indexMatch) && containerMatch) {
1657
+ const position = positionMatch ? parseInt(positionMatch[1]) : parseInt(indexMatch[1])
1658
+ const containerId = containerMatch[1]
1659
+ console.log(
1660
+ '[TVFocusManager] 尝试通过 FastListView 恢复焦点,容器:',
1661
+ containerId,
1662
+ '位置:',
1663
+ position
1664
+ )
1665
+
1666
+ // 找到 FastListView 容器
1667
+ const container = document.getElementById(containerId)
1668
+ if (container && container.__fastListViewInstance) {
1669
+ const fastList = container.__fastListViewInstance
1670
+ // 延迟调用 requestChildFocus,等待元素渲染完成
1671
+ setTimeout(() => {
1672
+ fastList.requestChildFocus(position)
1673
+ }, 100)
1674
+ return true
1675
+ }
1676
+ }
1677
+
1678
+ console.log('[TVFocusManager] 无法找到元素:', selector)
1679
+ return false
1680
+ }
1681
+
1682
+ /**
1683
+ * 找到元素所在的 ESPageRootView
1684
+ */
1685
+ _findPageRootView(element) {
1686
+ if (!element) return null
1687
+ let current = element
1688
+ while (current && current !== document.body) {
1689
+ const componentName = current.getAttribute('data-component-name')
1690
+ if (componentName === 'ESPageRootView') {
1691
+ return current
1692
+ }
1693
+ current = current.parentElement
1694
+ }
1695
+ return null
1696
+ }
1697
+
1698
+ /**
1699
+ * 恢复上一个页面的焦点(当页面被删除时调用)
1700
+ * 由 patches.js 中的 deleteNode 拦截触发
1701
+ */
1702
+ restorePreviousPageFocus() {
1703
+ console.log('[TVFocusManager] restorePreviousPageFocus 被调用')
1704
+
1705
+ // 清除当前焦点
1706
+ this.clearFocus()
1707
+
1708
+ // 更新可聚焦元素列表
1709
+ this.updateFocusableElements()
1710
+
1711
+ // 尝试从当前页面的 PageRootView 恢复焦点
1712
+ if (this._restoreFocusFromPageRootView()) {
1713
+ console.log('[TVFocusManager] 成功恢复上一个页面的焦点')
1714
+ return
1715
+ }
1716
+
1717
+ // 如果没有保存的焦点,聚焦第一个元素
1718
+ if (this.focusableElements.length > 0) {
1719
+ console.log('[TVFocusManager] 没有保存的焦点,聚焦第一个元素')
1720
+ this._doFocusElement(this.focusableElements[0])
1721
+ }
1722
+ }
1723
+
1434
1724
  _maybeNotifyFastListItemFocusChanged(element, hasFocus) {
1435
1725
  if (!element) return
1436
1726
  let parent = element.parentElement
@@ -10,6 +10,29 @@
10
10
  // 是否启用自动代理(仅开发环境)
11
11
  const ENABLE_AUTO_PROXY = process.env.NODE_ENV === 'development'
12
12
 
13
+ // 根据文件扩展名获取 Content-Type
14
+ function getContentType(path) {
15
+ const ext = path.split('.').pop().toLowerCase()
16
+ const types = {
17
+ js: 'application/javascript',
18
+ json: 'application/json',
19
+ css: 'text/css',
20
+ html: 'text/html',
21
+ png: 'image/png',
22
+ jpg: 'image/jpeg',
23
+ jpeg: 'image/jpeg',
24
+ gif: 'image/gif',
25
+ webp: 'image/webp',
26
+ svg: 'image/svg+xml',
27
+ woff: 'font/woff',
28
+ woff2: 'font/woff2',
29
+ ttf: 'font/ttf',
30
+ mp3: 'audio/mpeg',
31
+ mp4: 'video/mp4',
32
+ }
33
+ return types[ext] || 'application/octet-stream'
34
+ }
35
+
13
36
  // 需要代理的域名列表(可通过环境变量配置)
14
37
  const PROXY_DOMAINS = (process.env.VUE_APP_PROXY_DOMAINS || '')
15
38
  .split(',')
@@ -99,19 +122,43 @@ export function initAutoProxy() {
99
122
  window.fetch = function (input, init = {}) {
100
123
  let url = typeof input === 'string' ? input : input.url
101
124
 
125
+ // 处理 hpfile:// 协议
126
+ if (typeof url === 'string' && url.startsWith('hpfile://')) {
127
+ const converted = url.replace(/^hpfile:\/\/\.?\//, '/').replace(/^hpfile:\/\//, '/')
128
+ console.log('[AutoProxy] fetch hpfile:// converted:', url, '->', converted)
129
+
130
+ // 尝试从 VirtualFS 加载
131
+ if (window.__VIRTUAL_FS__ && window.__VIRTUAL_FS__.has(converted)) {
132
+ const content = window.__VIRTUAL_FS__.get(converted)
133
+ const blob = new Blob([content], { type: getContentType(converted) })
134
+ console.log('[AutoProxy] fetch loading from VirtualFS:', converted)
135
+ return Promise.resolve(
136
+ new Response(blob, {
137
+ status: 200,
138
+ headers: { 'Content-Type': getContentType(converted) },
139
+ })
140
+ )
141
+ }
142
+
143
+ // 返回新的 URL
144
+ if (typeof input === 'string') {
145
+ input = converted
146
+ } else if (input instanceof Request) {
147
+ input = new Request(converted, init)
148
+ }
149
+ }
150
+
102
151
  if (shouldProxy(url)) {
103
152
  const proxyUrlStr = toProxyUrl(url)
104
153
 
105
- // 更新 input
106
154
  if (typeof input === 'string') {
107
155
  input = proxyUrlStr
108
156
  } else if (input instanceof Request) {
109
- // 创建新的 Request 对象
110
157
  input = new Request(proxyUrlStr, {
111
158
  method: input.method,
112
159
  headers: input.headers,
113
160
  body: input.body,
114
- mode: 'cors', // 使用代理时改为同源
161
+ mode: 'cors',
115
162
  credentials: input.credentials,
116
163
  cache: input.cache,
117
164
  redirect: input.redirect,
@@ -142,6 +189,25 @@ export function initAutoProxy() {
142
189
  Object.defineProperty(HTMLImageElement.prototype, 'src', {
143
190
  ...originalImageSrcDescriptor,
144
191
  set(value) {
192
+ // 处理 hpfile:// 协议
193
+ if (typeof value === 'string' && value.startsWith('hpfile://')) {
194
+ const converted = value
195
+ .replace(/^hpfile:\/\/\.?\//, '/') // hpfile://./assets/xxx -> /assets/xxx
196
+ .replace(/^hpfile:\/\//, '/') // hpfile://assets/xxx -> /assets/xxx
197
+ console.log('[AutoProxy] hpfile:// converted:', value, '->', converted)
198
+
199
+ // 尝试从全局 VirtualFS 加载
200
+ if (window.__VIRTUAL_FS__ && window.__VIRTUAL_FS__.has(converted)) {
201
+ const content = window.__VIRTUAL_FS__.get(converted)
202
+ const blob = new Blob([content], { type: getContentType(converted) })
203
+ const url = URL.createObjectURL(blob)
204
+ console.log('[AutoProxy] Loading from VirtualFS:', converted)
205
+ return originalImageSrcDescriptor.set.call(this, url)
206
+ }
207
+
208
+ return originalImageSrcDescriptor.set.call(this, converted)
209
+ }
210
+
145
211
  if (value.startsWith('http://127.0.0.1:38989')) {
146
212
  return originalImageSrcDescriptor.set.call(
147
213
  this,
@@ -9,6 +9,7 @@ import { runtimeDeviceModuleInstance as RuntimeDeviceModule } from '../modules/R
9
9
  import { esToastModuleInstance as ESToastModule } from '../modules/ESToastModule'
10
10
  import { ESIJKAudioPlayerModule } from '../modules/ESIJKAudioPlayerModule'
11
11
  import { esNetworkSpeedModuleInstance as ESNetworkSpeedModule } from '../modules/ESNetworkSpeedModule'
12
+ import { normalizeStyleBackgroundEntries } from './styleBackground'
12
13
 
13
14
  // Create singleton instance for audio player
14
15
  const audioPlayerModule = new ESIJKAudioPlayerModule(null)
@@ -236,6 +237,27 @@ export function patchCallNative(engine) {
236
237
  const [rootViewId, nodes] = args
237
238
 
238
239
  if (Array.isArray(nodes)) {
240
+ // 检查是否有 ESPageRootView 被创建,如果有说明要进入新页面
241
+ let hasNewPage = false
242
+ nodes.forEach((node) => {
243
+ let nodeData = node
244
+ if (Array.isArray(node)) {
245
+ nodeData = node[0]
246
+ }
247
+ if (nodeData && nodeData.name === 'ESPageRootView') {
248
+ hasNewPage = true
249
+ }
250
+ })
251
+
252
+ // 如果要创建新页面,先保存当前页面的焦点状态
253
+ if (hasNewPage) {
254
+ console.log('[Web Renderer] 检测到新页面创建,保存当前页面焦点')
255
+ const focusManager = global.__TV_FOCUS_MANAGER__
256
+ if (focusManager && typeof focusManager.saveCurrentPageFocus === 'function') {
257
+ focusManager.saveCurrentPageFocus()
258
+ }
259
+ }
260
+
239
261
  const processedNodes = nodes.map((node) => {
240
262
  let nodeData = node
241
263
  let props = null
@@ -307,6 +329,38 @@ export function patchCallNative(engine) {
307
329
  return originalCallNative(moduleName, methodName, rootViewId, processedNodes)
308
330
  }
309
331
  }
332
+
333
+ if (methodName === 'deleteNode') {
334
+ const [rootViewId, nodes] = args
335
+
336
+ if (Array.isArray(nodes)) {
337
+ // 检查是否有 ESPageRootView 被删除
338
+ nodes.forEach((node) => {
339
+ let nodeData = node
340
+ if (Array.isArray(node)) {
341
+ nodeData = node[0]
342
+ }
343
+ if (nodeData && typeof nodeData === 'object') {
344
+ console.log('[Web Renderer] Deleting node:', nodeData.id, 'name:', nodeData.name)
345
+
346
+ // 检查是否是页面被删除
347
+ if (nodeData.name === 'ESPageRootView') {
348
+ console.log(
349
+ '[Web Renderer] ESPageRootView 被删除,通知焦点管理器恢复上一个页面焦点'
350
+ )
351
+ // 通知 TVFocusManager 恢复焦点
352
+ const focusManager = global.__TV_FOCUS_MANAGER__
353
+ if (focusManager && typeof focusManager.restorePreviousPageFocus === 'function') {
354
+ // 延迟执行,确保 DOM 更新完成
355
+ setTimeout(() => {
356
+ focusManager.restorePreviousPageFocus()
357
+ }, 50)
358
+ }
359
+ }
360
+ }
361
+ })
362
+ }
363
+ }
310
364
  }
311
365
 
312
366
  // Handle ESToastModule - redirect to web toast adapter
@@ -755,7 +809,7 @@ function patchHippyWebSetElementStyle(engine) {
755
809
  if (HippyWeb && HippyWeb.setElementStyle && !HippyWeb._setElementStylePatched) {
756
810
  const originalSetElementStyle = HippyWeb.setElementStyle.bind(HippyWeb)
757
811
  HippyWeb.setElementStyle = (element, styleObj) => {
758
- const cleanedStyle = extractFocusStyles(element, styleObj)
812
+ const cleanedStyle = normalizeStyleBackgroundEntries(extractFocusStyles(element, styleObj))
759
813
  originalSetElementStyle(element, cleanedStyle)
760
814
  }
761
815
  HippyWeb._setElementStylePatched = true
@@ -850,7 +904,7 @@ export function patchUIManager(engine) {
850
904
  props.style.backgroundColor = '#000000'
851
905
  }
852
906
 
853
- props.style = extractFocusStyles(view.dom, props.style)
907
+ props.style = normalizeStyleBackgroundEntries(extractFocusStyles(view.dom, props.style))
854
908
  }
855
909
 
856
910
  const normalizeClass = (value) => {
@@ -902,7 +956,7 @@ export function patchUIManager(engine) {
902
956
  if (HippyWeb?.setElementStyle) {
903
957
  HippyWeb.setElementStyle(view.dom, props.style)
904
958
  } else {
905
- Object.assign(view.dom.style, props.style)
959
+ Object.assign(view.dom.style, normalizeStyleBackgroundEntries(props.style))
906
960
  }
907
961
  }
908
962
  return
@@ -0,0 +1,90 @@
1
+ export function normalizeHpfileAssetUrl(value) {
2
+ if (typeof value !== 'string') return value
3
+ return value.replace(/^hpfile:\/\/\.?\//, '/').replace(/^hpfile:\/\//, '/')
4
+ }
5
+
6
+ function getContentType(path) {
7
+ const ext = String(path).split('.').pop().toLowerCase()
8
+ const types = {
9
+ png: 'image/png',
10
+ jpg: 'image/jpeg',
11
+ jpeg: 'image/jpeg',
12
+ gif: 'image/gif',
13
+ webp: 'image/webp',
14
+ svg: 'image/svg+xml',
15
+ }
16
+ return types[ext] || 'application/octet-stream'
17
+ }
18
+
19
+ function resolveVirtualFsAssetUrl(value) {
20
+ if (typeof value !== 'string') return value
21
+ const normalizedUrl = normalizeHpfileAssetUrl(value)
22
+ if (!normalizedUrl.startsWith('/assets/')) return normalizedUrl
23
+
24
+ const virtualFs = window.__VIRTUAL_FS__
25
+ if (!virtualFs || typeof virtualFs.get !== 'function') return normalizedUrl
26
+
27
+ const cache = (window.__WEB_VFS_BLOB_URL_CACHE__ = window.__WEB_VFS_BLOB_URL_CACHE__ || new Map())
28
+ if (cache.has(normalizedUrl)) {
29
+ return cache.get(normalizedUrl)
30
+ }
31
+
32
+ const content = virtualFs.get(normalizedUrl)
33
+ if (!content) return normalizedUrl
34
+
35
+ const blobUrl = URL.createObjectURL(new Blob([content], { type: getContentType(normalizedUrl) }))
36
+ cache.set(normalizedUrl, blobUrl)
37
+ return blobUrl
38
+ }
39
+
40
+ function replaceCssUrls(value) {
41
+ return value.replace(/url\(\s*(['"]?)([^'")\s]+)\1\s*\)/gi, (match, quote, urlValue) => {
42
+ const resolvedUrl = resolveVirtualFsAssetUrl(urlValue)
43
+ if (resolvedUrl === urlValue) return match
44
+ return `url(${resolvedUrl})`
45
+ })
46
+ }
47
+
48
+ export function normalizeBackgroundStyleValue(key, value) {
49
+ if (typeof value !== 'string') return value
50
+
51
+ const lowerKey = String(key).toLowerCase()
52
+ if (
53
+ lowerKey !== 'backgroundimage' &&
54
+ lowerKey !== 'background-image' &&
55
+ lowerKey !== 'background'
56
+ ) {
57
+ return value
58
+ }
59
+
60
+ const trimmed = value.trim()
61
+ if (!trimmed) return value
62
+
63
+ if (/url\(/i.test(trimmed)) {
64
+ return replaceCssUrls(trimmed)
65
+ }
66
+
67
+ const resolvedUrl = resolveVirtualFsAssetUrl(trimmed)
68
+ if (resolvedUrl !== trimmed) {
69
+ return `url(${resolvedUrl})`
70
+ }
71
+
72
+ return value
73
+ }
74
+
75
+ export function normalizeStyleBackgroundEntries(styleObj) {
76
+ if (!styleObj || typeof styleObj !== 'object') return styleObj
77
+
78
+ let nextStyle = styleObj
79
+ ;['backgroundImage', 'background', 'background-image'].forEach((key) => {
80
+ if (!Object.prototype.hasOwnProperty.call(styleObj, key)) return
81
+ const nextValue = normalizeBackgroundStyleValue(key, styleObj[key])
82
+ if (nextValue === styleObj[key]) return
83
+ if (nextStyle === styleObj) {
84
+ nextStyle = { ...styleObj }
85
+ }
86
+ nextStyle[key] = nextValue
87
+ })
88
+
89
+ return nextStyle
90
+ }
@@ -1,3 +1,5 @@
1
+ import { normalizeBackgroundStyleValue } from './styleBackground'
2
+
1
3
  export function extractTemplateValueByPath(propPath, data) {
2
4
  let value = propPath.split('.').reduce((obj, key) => {
3
5
  return obj && obj[key] !== undefined ? obj[key] : undefined
@@ -61,6 +63,10 @@ function _applyFlexStyleToElement(element, styleObj) {
61
63
  Object.keys(styleObj).forEach((key) => {
62
64
  let value = styleObj[key]
63
65
 
66
+ if (key === 'backgroundImage' || key === 'background') {
67
+ value = normalizeBackgroundStyleValue(key, value)
68
+ }
69
+
64
70
  if (typeof value === 'number') {
65
71
  const needsPx = [
66
72
  'width',
@@ -271,16 +277,16 @@ function _evaluateShowIf(expression, data) {
271
277
 
272
278
  // Simple property access - truthy check
273
279
  const value = extractTemplateValueByPath(expr, data)
274
- console.log(
275
- '[templateBinding] _evaluateShowIf: expr=',
276
- expr,
277
- 'value=',
278
- value,
279
- 'typeof=',
280
- typeof value,
281
- 'result=',
282
- !!value
283
- )
280
+ // console.log(
281
+ // '[templateBinding] _evaluateShowIf: expr=',
282
+ // expr,
283
+ // 'value=',
284
+ // value,
285
+ // 'typeof=',
286
+ // typeof value,
287
+ // 'result=',
288
+ // !!value
289
+ // )
284
290
  return !!value
285
291
  }
286
292
 
@@ -452,38 +458,38 @@ export function bindTemplateDataToNode(node, data, options = {}) {
452
458
  const allAttrs = Array.from(node.attributes)
453
459
  .map((a) => `${a.name}="${a.value}"`)
454
460
  .join(', ')
455
- console.log('[templateBinding] Node:', node.tagName, 'All attrs:', allAttrs)
461
+ // console.log('[templateBinding] Node:', node.tagName, 'All attrs:', allAttrs)
456
462
  let showIfAttr = node.getAttribute('showIf') || node.getAttribute('showif')
457
- console.log('[templateBinding] Checking showIf on node:', node.tagName, 'showIf:', showIfAttr)
463
+ // console.log('[templateBinding] Checking showIf on node:', node.tagName, 'showIf:', showIfAttr)
458
464
  if (showIfAttr) {
459
465
  // Store the expression for re-evaluation when data changes
460
466
  node.setAttribute('data-showif-expr', showIfAttr)
461
467
  // Remove both possible attribute names to ensure cleanup
462
468
  node.removeAttribute('showIf')
463
469
  node.removeAttribute('showif')
464
- console.log('[templateBinding] Stored showIf expr:', showIfAttr)
470
+ // console.log('[templateBinding] Stored showIf expr:', showIfAttr)
465
471
  }
466
472
 
467
473
  // Check for stored showIf expression (for re-evaluation on data updates)
468
474
  const storedShowIfExpr = node.getAttribute('data-showif-expr')
469
475
  if (storedShowIfExpr) {
470
- console.log(
471
- '[templateBinding] Found stored showIf expr:',
472
- storedShowIfExpr,
473
- 'data:',
474
- combinedData
475
- )
476
+ // console.log(
477
+ // '[templateBinding] Found stored showIf expr:',
478
+ // storedShowIfExpr,
479
+ // 'data:',
480
+ // combinedData
481
+ // )
476
482
  const shouldShow = _evaluateShowIf(storedShowIfExpr, combinedData)
477
483
  const prevResult = node.getAttribute('data-showif-result')
478
484
  const newResult = shouldShow === false ? 'false' : 'true'
479
- console.log(
480
- '[templateBinding] showIf result:',
481
- shouldShow,
482
- 'prev:',
483
- prevResult,
484
- 'new:',
485
- newResult
486
- )
485
+ // console.log(
486
+ // '[templateBinding] showIf result:',
487
+ // shouldShow,
488
+ // 'prev:',
489
+ // prevResult,
490
+ // 'new:',
491
+ // newResult
492
+ // )
487
493
 
488
494
  // Only update if result changed
489
495
  if (prevResult !== newResult) {
package/src/index.js CHANGED
@@ -7,15 +7,6 @@ export * from './core'
7
7
  // Components
8
8
  export * from './components'
9
9
 
10
- // Import functions needed for initWebRenderer
11
- import {
12
- setupSceneBuilder,
13
- applyAllPatches,
14
- TVFocusManager,
15
- initAutoProxy,
16
- initAsyncLocalStorage,
17
- } from './core'
18
-
19
10
  // Create component registry for web renderer
20
11
  import { HippyWebEngine } from '@hippy/web-renderer'
21
12
  import { QtView, createNamedComponent } from './components/QtView'
@@ -252,100 +243,4 @@ export function startWebEngine(engine) {
252
243
  console.log('[Web Renderer] Engine started')
253
244
  }
254
245
 
255
- // Global engine instance for convenience functions
256
- let _webEngine = null
257
-
258
- /**
259
- * Initialize the web renderer (convenience function)
260
- * Creates and prepares the engine, applies patches
261
- * This matches the initialization flow in main-web.js
262
- */
263
- export function initWebRenderer() {
264
- if (_webEngine) {
265
- console.log('[Web Renderer] Engine already initialized')
266
- return _webEngine
267
- }
268
-
269
- console.log('[Web Renderer] === Starting initialization ===')
270
-
271
- // Step 0: Initialize async localStorage (must be first)
272
- initAsyncLocalStorage()
273
-
274
- // Step 0.1: Initialize auto proxy for development
275
- initAutoProxy()
276
-
277
- // Step 0.2: Register IJKPlayerComponent for web video
278
- global.__WEB_COMPONENT__ = global.__WEB_COMPONENTS__ || {}
279
- global.__WEB_COMPONENTS__['IJKPlayerComponent'] = IJKPlayerComponent
280
-
281
- // Step 1: Setup SceneBuilder (must be done before main.ts loads)
282
- setupSceneBuilder()
283
-
284
- console.log('[Web Renderer] Component mappings:', Object.keys(quicktvuiComponents))
285
-
286
- // Step 2: Create the web engine
287
- _webEngine = createWebEngine()
288
-
289
- // Step 3: Apply all patches BEFORE starting
290
- applyAllPatches(_webEngine)
291
-
292
- // Step 4: Initialize TV Focus Manager
293
- const focusManager = new TVFocusManager()
294
- global.__TV_FOCUS_MANAGER__ = focusManager
295
- console.log('[Web Renderer] TVFocusManager initialized')
296
-
297
- // Step 5: Inject global CSS to match Android layout behavior
298
- const styleEl = document.createElement('style')
299
- styleEl.id = 'web-platform-reset'
300
- styleEl.textContent = `
301
- /* Web-Android Layout Compatibility Reset */
302
-
303
- /* Disable flex cross-axis stretching (Android behavior) */
304
- /* In Android, children don't auto-fill parent's cross dimension */
305
- #app,
306
- #app * {
307
- align-items: flex-start;
308
- }
309
-
310
- /* Elements with explicit style should override the reset */
311
- [style*="align-items"] {
312
- align-items: var(--align-items, center) !important;
313
- }
314
- `
315
- document.head.appendChild(styleEl)
316
- console.log('[Web Renderer] Global CSS reset injected for Android-compatible layout')
317
-
318
- console.log('[Web Renderer] Initialization complete')
319
- return _webEngine
320
- }
321
-
322
- /**
323
- * Start the web renderer (convenience function)
324
- * Starts the previously initialized engine
325
- */
326
- export function startWebRenderer() {
327
- if (!_webEngine) {
328
- console.log('[Web Renderer] No engine found, initializing...')
329
- initWebRenderer()
330
- }
331
-
332
- console.log('[Web Renderer] Starting engine...')
333
- startWebEngine(_webEngine)
334
- return _webEngine
335
- }
336
-
337
- /**
338
- * Get the current web engine instance
339
- */
340
- export function getWebEngine() {
341
- return _webEngine
342
- }
343
-
344
- /**
345
- * Set the web engine instance
346
- */
347
- export function setWebEngine(engine) {
348
- _webEngine = engine
349
- }
350
-
351
- export { APP_NAME, IJKPlayerComponent }
246
+ export { APP_NAME }