@quicktvui/web-renderer 1.0.7 → 1.0.9
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 +1 -1
- package/src/components/QtBaseComponent.js +66 -84
- package/src/components/QtFastListView.js +58 -4
- package/src/components/QtImage.js +3 -0
- package/src/components/QtTransitionImage.js +3 -0
- package/src/components/QtView.js +26 -1
- package/src/core/TVFocusManager.js +320 -30
- package/src/core/autoProxy.js +69 -3
- package/src/core/patches.js +57 -3
- package/src/core/styleBackground.js +90 -0
- package/src/core/templateBinding.js +33 -27
- package/src/index.js +0 -105
package/package.json
CHANGED
|
@@ -1,16 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* QtBaseComponent - Base class for all QuickTV UI components
|
|
3
|
-
* Provides common property handling (visible, visibility,
|
|
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
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
979
|
-
|
|
980
|
-
|
|
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
|
|
package/src/components/QtView.js
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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
|
-
|
|
1238
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
package/src/core/autoProxy.js
CHANGED
|
@@ -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,
|
package/src/core/patches.js
CHANGED
|
@@ -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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
246
|
export { APP_NAME, IJKPlayerComponent }
|