@ray-js/ipc-player-integration 0.0.50-beta.0 → 0.0.50-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +11 -11
  2. package/lib/ctx/ctx.composition.js +5 -5
  3. package/lib/features/initPlayerWidgets/index.d.ts +0 -3
  4. package/lib/features/initPlayerWidgets/index.js +1 -2
  5. package/lib/hooks/useBattery/index.js +7 -1
  6. package/lib/iconfont/iconfont.css +127 -7
  7. package/lib/iconfont/iconfont.js +10 -10
  8. package/lib/iconfont/iconfont.json +233 -23
  9. package/lib/iconfont/iconfont.ttf +0 -0
  10. package/lib/iconfont/iconfont.woff +0 -0
  11. package/lib/iconfont/iconfont.woff2 +0 -0
  12. package/lib/interface.d.ts +1 -1
  13. package/lib/interface.js +1 -1
  14. package/lib/ports.output.d.ts +13 -0
  15. package/lib/ports.output.js +13 -0
  16. package/lib/ui/bottomLeftContent.js +141 -27
  17. package/lib/ui/bottomRightContent.d.ts +6 -1
  18. package/lib/ui/bottomRightContent.js +43 -5
  19. package/lib/ui/ui.js +58 -4
  20. package/lib/ui/ui.less +17 -0
  21. package/lib/widgets/battery/battery.js +11 -3
  22. package/lib/widgets/battery/battery.less +11 -0
  23. package/lib/widgets/battery/batteryFull.js +11 -3
  24. package/lib/widgets/zoomTurntable/constants.d.ts +44 -12
  25. package/lib/widgets/zoomTurntable/constants.js +61 -17
  26. package/lib/widgets/zoomTurntable/lensModel.test.js +1 -1
  27. package/lib/widgets/zoomTurntable/lensSkill.d.ts +1 -1
  28. package/lib/widgets/zoomTurntable/lensSkill.js +1 -1
  29. package/lib/widgets/zoomTurntable/turntable/index.js +4 -1
  30. package/lib/widgets/zoomTurntable/turntable/rjs/index.rjs +138 -9
  31. package/lib/widgets/zoomTurntable/turntable/rjs/index.tyml +1 -0
  32. package/lib/widgets/zoomTurntable/useLensZoom.d.ts +3 -4
  33. package/lib/widgets/zoomTurntable/useLensZoom.js +94 -12
  34. package/lib/widgets/zoomTurntable/zoomTurntableDock.js +76 -4
  35. package/lib/widgets/zoomTurntable/zoomTurntableEntry.d.ts +0 -3
  36. package/lib/widgets/zoomTurntable/zoomTurntableEntry.js +20 -5
  37. package/lib/widgets/zoomTurntable/zoomTurntableEntry.less +11 -1
  38. package/package.json +2 -2
@@ -1,6 +1,10 @@
1
1
  import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
2
2
  import "core-js/modules/esnext.iterator.constructor.js";
3
+ import "core-js/modules/esnext.iterator.every.js";
4
+ import "core-js/modules/esnext.iterator.filter.js";
3
5
  import "core-js/modules/esnext.iterator.find.js";
6
+ import "core-js/modules/esnext.iterator.map.js";
7
+ import "core-js/modules/esnext.iterator.reduce.js";
4
8
  import React, { useEffect, useMemo, useRef, useState } from 'react';
5
9
  import { CoverView, ScrollView, View } from '@ray-js/ray';
6
10
  import clsx from 'clsx';
@@ -12,8 +16,19 @@ import { bottomLeftContentResize, pauseTimeToHideAllComponent, startTimeToHideAl
12
16
  // 用唯一 className 在 createSelectorQuery 中区分多个实例,避免互相干扰
13
17
  let scrollClsSeq = 0;
14
18
 
15
- // 竖屏下单个 widget 的占位宽度:icon 22px(--iconPlayerSize) + .bottom-left-item-container 左右各 12px
16
- const VERTICAL_WIDGET_WIDTH = 46;
19
+ // 竖屏最多平铺展示的 widget 数:<= 4 个全部平铺,> 4 个从第 5 个开始横滑
20
+ const VERTICAL_FLAT_MAX = 4;
21
+
22
+ // 竖屏滚动档:可视区在前 4 项之外多留出的露出区宽度。
23
+ // 这段同时也是箭头的落位区 —— 箭头贴着内容压在第 5 项露出的一角上(和改动前的观感一致),
24
+ // 因此至少要放得下箭头本体 26 + 左右各 4 的间隙。
25
+ const VERTICAL_PEEK_WIDTH = 34;
26
+
27
+ // 平铺档内容与右下角按钮之间的最小安全间距
28
+ const VERTICAL_FLAT_SAFE_GAP = 8;
29
+
30
+ // 左下角子元素统一类名(由 ui.tsx 渲染时挂上),用于 selectAll 逐项测量
31
+ const ITEM_CLASS = 'bottom-left-item-container';
17
32
  const BottomLeftContent = _ref => {
18
33
  let {
19
34
  ctx,
@@ -67,6 +82,9 @@ const BottomLeftContent = _ref => {
67
82
 
68
83
  // 横向滚动状态:内容溢出且未滑到对应边缘时,显示左 / 右箭头提示
69
84
  const idRef = useRef(++scrollClsSeq);
85
+ // wrap 是左下角的可用区(left:0 ~ right:reservedRight),宽度不随 ScrollView 收窄而变,
86
+ // 因此用它、而不是 ScrollView 自身,作为“能摆多宽”的基准,避免收窄与分档互相依赖
87
+ const wrapCls = `ipc-bl-wrap-${idRef.current}`;
70
88
  const scrollCls = `ipc-bl-scroll-${idRef.current}`;
71
89
  const contentCls = `ipc-bl-scroll-content-${idRef.current}`;
72
90
  // 内容末尾哨兵节点:用它的真实横坐标反推子项总宽,避免内容容器被撑满导致测不出溢出
@@ -74,6 +92,12 @@ const BottomLeftContent = _ref => {
74
92
  const [scrollLeft, setScrollLeft] = useState(0);
75
93
  const [clientWidth, setClientWidth] = useState(0);
76
94
  const [contentWidth, setContentWidth] = useState(0);
95
+ // 左下角可用区宽度(wrap 的实测宽)
96
+ const [wrapWidth, setWrapWidth] = useState(0);
97
+ // 真实渲染出来的每个 widget 的宽度:长度即真实可见个数。
98
+ // 不能用 bottomLeftContent 的配置长度推算 —— 组件一直在列表里,只是内部按 dp /
99
+ // 异步能力 return null,长度不变。
100
+ const [itemWidths, setItemWidths] = useState([]);
77
101
  // 点击箭头时的受控滚动目标;仅在程序化滚动期间为数值,结束后置回 undefined,
78
102
  // 避免持续受控导致用户手动拖动被回弹
79
103
  const [scrollTo, setScrollTo] = useState(undefined);
@@ -94,6 +118,8 @@ const BottomLeftContent = _ref => {
94
118
  const scrollEndTimerRef = useRef();
95
119
  // 测量时需要叠加当前滚动量,用 ref 避免闭包拿到过期值
96
120
  const scrollLeftRef = useRef(0);
121
+ // 仅用于在可见项数变化时打一条日志,便于真机核对分档结果
122
+ const lastCountRef = useRef(-1);
97
123
  useEffect(() => {
98
124
  setScrollResetKey(k => k + 1);
99
125
  setScrollLeft(0);
@@ -102,6 +128,8 @@ const BottomLeftContent = _ref => {
102
128
  // 清零上一屏的测量值,避免用旧 clientWidth/contentWidth 在重新测量前误判溢出导致箭头闪现
103
129
  setClientWidth(0);
104
130
  setContentWidth(0);
131
+ setWrapWidth(0);
132
+ setItemWidths([]);
105
133
  }, [screenType]);
106
134
 
107
135
  // 程序化滚动目标只在动画期间短暂受控,之后释放为非受控,避免回弹影响手动拖动
@@ -112,13 +140,52 @@ const BottomLeftContent = _ref => {
112
140
  const t = setTimeout(() => setScrollTo(undefined), 600);
113
141
  return () => clearTimeout(t);
114
142
  }, [scrollTo]);
143
+ const isVertical = screenType === 'vertical';
115
144
  const paddingLeftPx = 0;
116
145
  // 左侧留白必须挂内容容器:ScrollView 的 host 不是滚动器,host 上的 padding 内容滚不进去,
117
146
  // 最左 icon 贴不到屏幕边缘
118
- const contentPaddingLeftPx = screenType === 'vertical' ? 0 : 25;
119
- // 滚动末端的安全距离:滑到最右时最后一个元素与右边缘留更宽松的间距(横屏更宽)
120
- const paddingRightPx = screenType === 'vertical' ? 22 : 32;
121
- const paddingBottomPx = screenType === 'vertical' ? 0 : 25;
147
+ const contentPaddingLeftPx = isVertical ? 0 : 25;
148
+ const paddingBottomPx = isVertical ? 0 : 25;
149
+
150
+ // ---- 竖屏分档:<= 4 个平铺,> 4 个锁在前 4 项并从第 5 项起横滑 ----
151
+ // 分档依据是“真实渲染了几个”,不是像素比对,因此不会再出现少量元素被误折叠。
152
+ const measuredCount = itemWidths.length;
153
+ const measuredItemsWidth = itemWidths.reduce((sum, w) => sum + w, 0);
154
+ // 可用宽以 wrap 为准;wrap 还没测出来时退回 ScrollView 自身宽度
155
+ const availableWidth = wrapWidth || clientWidth;
156
+ // 自然排布(不收窄)时内容需要的总宽。
157
+ // 逐项宽度不含内容容器的左留白,要补上 contentPaddingLeftPx 才和哨兵反推的口径一致(横屏 25px)
158
+ const naturalWidth = measuredItemsWidth > 0 ? measuredItemsWidth + contentPaddingLeftPx : contentWidth;
159
+ // “放得下”这层保险:小屏 + 宽 widget(清晰度文案 / 试用胶囊)会让 4 个也摆不下,
160
+ // 这种情况一样走滚动,避免平铺后压到右下角按钮
161
+ const contentFits = !(availableWidth > 0) || !(naturalWidth > 0) || naturalWidth + VERTICAL_FLAT_SAFE_GAP <= availableWidth;
162
+ // 竖屏三档:
163
+ // flat —— 个数 <= 4 且放得下:全部平铺,不滚动、无箭头、无淡出
164
+ // compact —— 个数 <= 4 但放不下(小屏 / 宽 widget):用满可用宽,允许轻微横滑,
165
+ // 但不出箭头,尽量把这 4 个展示全
166
+ // scroll —— 个数 > 4:可视区锁在“前 4 项 + 露出区”,箭头压在露出区上
167
+ // 个数测不出来时(原生层不支持 selectAll)退回“纯按内容溢出滚动”,不锁宽,
168
+ // 避免估算失准把本来放得下的元素裁掉。
169
+ const isFlat = isVertical && (measuredCount > 0 ? measuredCount <= VERTICAL_FLAT_MAX && contentFits : contentFits);
170
+ const isCompact = isVertical && !isFlat && measuredCount > 0 && measuredCount <= VERTICAL_FLAT_MAX;
171
+ // 前 4 项实际累计宽(元素按自身内容撑开,不做等分)
172
+ const firstFourWidth = itemWidths.slice(0, VERTICAL_FLAT_MAX).reduce((sum, w) => sum + w, 0);
173
+ // 滚动档可视宽 = 前 4 项宽 + 第 5 项的露出区(箭头就压在这段上)
174
+ const verticalScrollWidth = (() => {
175
+ if (!(isVertical && !isFlat && !isCompact) || measuredCount === 0) return 0;
176
+ const want = firstFourWidth + VERTICAL_PEEK_WIDTH;
177
+ if (!(availableWidth > 0)) return want;
178
+ // 前 4 项 + 露出区塞不下 —— 前 4 项里有宽 widget(焦距转盘的倍率胶囊、清晰度文案)时很常见。
179
+ // 这时锁宽只会把第 4 项裁掉,改为用满可用宽,优先保证前 4 项完整
180
+ if (want > availableWidth) return 0;
181
+ return want;
182
+ })();
183
+ // 竖屏只有锁住宽度时箭头才有专属的露出区可压;否则箭头一定会盖到前 4 项上,索性不出
184
+
185
+ // 滚动末端的安全距离:滑到最右时最后一个元素与右边缘留更宽松的间距(横屏更宽)。
186
+ // 只有真正锁宽滚动的那一档需要;平铺 / 紧凑 / 塞不下而没锁宽的情况,
187
+ // 留这段只会白白挤占可视宽,置 0 全部让给元素。
188
+ const paddingRightPx = isVertical ? verticalScrollWidth > 0 ? 22 : 0 : 32;
122
189
  const showBadge = showSmartImageQuality && showTrialBadge && canOpenSettings && gRawData && !isLowPhone && buttonState === 1 && trialRemainingSec > 0;
123
190
 
124
191
  // 测量滚动容器可视宽度与内部内容宽度。
@@ -132,17 +199,44 @@ const BottomLeftContent = _ref => {
132
199
  let attempts = 0;
133
200
  const measure = () => {
134
201
  const query = ty.createSelectorQuery();
202
+ query.select(`.${wrapCls}`).boundingClientRect();
135
203
  query.select(`.${scrollCls}`).boundingClientRect();
136
204
  query.select(`.${contentCls}`).boundingClientRect();
137
205
  query.select(`.${endCls}`).boundingClientRect();
206
+ // 逐项测量:一次拿到“真实渲染了几个 widget”以及每个的实际宽度。
207
+ // 原生层若不支持 selectAll,这里不入队,后续按哨兵总宽兜底走老路径。
208
+ try {
209
+ query.selectAll(`.${contentCls} .${ITEM_CLASS}`).boundingClientRect();
210
+ // 后代选择器不被原生层支持时会返回空,再用全局类名兜一次(同屏单实例场景足够)
211
+ query.selectAll(`.${ITEM_CLASS}`).boundingClientRect();
212
+ } catch (e) {
213
+ // ignore: 宿主不支持 selectAll,下面按哨兵总宽走老路径
214
+ }
138
215
  query.exec(res => {
139
- const scrollRect = res === null || res === void 0 ? void 0 : res[0];
140
- const contentRect = res === null || res === void 0 ? void 0 : res[1];
141
- const endRect = res === null || res === void 0 ? void 0 : res[2];
216
+ const wrapRect = res === null || res === void 0 ? void 0 : res[0];
217
+ const scrollRect = res === null || res === void 0 ? void 0 : res[1];
218
+ const contentRect = res === null || res === void 0 ? void 0 : res[2];
219
+ const endRect = res === null || res === void 0 ? void 0 : res[3];
220
+ const scopedItemRects = res === null || res === void 0 ? void 0 : res[4];
221
+ const globalItemRects = res === null || res === void 0 ? void 0 : res[5];
222
+ const itemRects = Array.isArray(scopedItemRects) && scopedItemRects.length ? scopedItemRects : globalItemRects;
223
+ const ww = (wrapRect === null || wrapRect === void 0 ? void 0 : wrapRect.width) || 0;
224
+ if (ww) {
225
+ setWrapWidth(ww);
226
+ }
142
227
  const cw = (scrollRect === null || scrollRect === void 0 ? void 0 : scrollRect.width) || 0;
143
228
  if (cw) {
144
229
  setClientWidth(cw);
145
230
  }
231
+ if (Array.isArray(itemRects)) {
232
+ const widths = itemRects.map(r => (r === null || r === void 0 ? void 0 : r.width) || 0).filter(w => w > 0);
233
+ if (widths.length !== lastCountRef.current) {
234
+ lastCountRef.current = widths.length;
235
+ console.log('[ipc-player] 左下角可见 widget 数 =', widths.length, widths);
236
+ }
237
+ // 值没变时保持同一引用,避免轮询期间无谓重渲染
238
+ setItemWidths(prev => prev.length === widths.length && prev.every((w, i) => w === widths[i]) ? prev : widths);
239
+ }
146
240
 
147
241
  // 优先用末尾哨兵反推子项真实总宽:
148
242
  // 哨兵视口 x = 滚动容器左 + 左padding + 子项总宽 - 当前 scrollLeft
@@ -177,7 +271,7 @@ const BottomLeftContent = _ref => {
177
271
  // reservedRight 决定 ScrollView 可视宽度(右下角内容异步加载后会从 0 变为实际宽度)
178
272
  reservedRight,
179
273
  // playState:视频就绪后 overlay 可能重新布局,需重新测量
180
- playState, bottomLeftContent === null || bottomLeftContent === void 0 ? void 0 : bottomLeftContent.length, shouldHide, showBadge, scrollCls, contentCls, scrollResetKey, resizeSignal]);
274
+ playState, bottomLeftContent === null || bottomLeftContent === void 0 ? void 0 : bottomLeftContent.length, shouldHide, showBadge, wrapCls, scrollCls, contentCls, scrollResetKey, resizeSignal]);
181
275
 
182
276
  // 子元素主动上报宽度变化 → 重跑测量,及时更新横滑箭头与右边缘淡出
183
277
  useEffect(() => {
@@ -203,15 +297,19 @@ const BottomLeftContent = _ref => {
203
297
  };
204
298
  }, [ctx.event]);
205
299
 
206
- // ScrollView 的实际可滚动宽度 = 左右 padding + 子元素总宽
207
- const totalContentWidth = contentWidth > 0 ? paddingLeftPx + contentWidth + paddingRightPx : 0;
208
- const isOverflow = totalContentWidth > clientWidth + 1;
209
- const atEnd = !isOverflow || scrollLeft + clientWidth >= totalContentWidth - 2;
300
+ // ScrollView 的实际可滚动宽度 = 左右 padding + 子元素总宽(逐项实测优先,哨兵总宽兜底)
301
+ const totalContentWidth = naturalWidth > 0 ? paddingLeftPx + naturalWidth + paddingRightPx : 0;
302
+ // 竖屏由“个数”决定是否溢出(平铺档恒不溢出);横屏保持原来的像素比对
303
+ const isOverflow = isVertical ? !isFlat : totalContentWidth > clientWidth + 1;
304
+ const scrollViewWidth = verticalScrollWidth > 0 ? verticalScrollWidth : clientWidth;
305
+ const atEnd = !isOverflow || scrollLeft + scrollViewWidth >= totalContentWidth - 2;
210
306
  const isDismissed = dismissed[screenType];
211
307
  const isButtonHint = scrollHintMode === 'button';
212
- // button 模式:溢出且未到末端即常显(不受 dismissed 影响);pulse 模式:保留拖动后消失的旧逻辑
308
+ // button 模式:溢出且未到末端即常显(不受 dismissed 影响);pulse 模式:保留拖动后消失的旧逻辑。
309
+ // compact 档(<= 4 个但摆不下)不出箭头:那点空间要留给元素本身,箭头只会遮更多
213
310
 
214
311
  // 右侧还有未滚到头的内容时,启用右边缘淡出,把没完全露出的元素渐隐隐藏(横竖屏都生效)
312
+ const fadeRightEdge = isOverflow && !atEnd;
215
313
 
216
314
  // 点击右箭头:直接滚动到最右端。
217
315
  // 给一个足够大的目标值,交由 ScrollView 自身夹取到真实最右端,避免测量误差导致滚不到底。
@@ -230,16 +328,30 @@ const BottomLeftContent = _ref => {
230
328
  const visibleRowHeight = containerHeight - paddingBottomPx;
231
329
  // button 模式用半透明圆做点击热区,比 pulse 提示大;下移半个差值以保持图标中心不变
232
330
  const hintSize = isButtonHint ? 26 : 16;
233
- // 竖屏 button 模式给 ScrollView 右侧留一条较窄的“箭头槽”:只裁掉内容最右一小段,
234
- // 箭头压住最后一个元素的少量边缘(不完全遮挡),整体更贴近内容
235
- const arrowGutter = isButtonHint && screenType === 'vertical' ? 12 : 0;
236
- // 竖屏再额外收窄一个 widget 的宽度,让可滑动区域整体左缩一格
237
- const verticalShrink = screenType === 'vertical' ? VERTICAL_WIDGET_WIDTH : 0;
238
- const scrollShrink = arrowGutter + verticalShrink;
239
- // 箭头贴滚动可视区右边缘放置(竖屏 4px / 横屏 6px),随收窄量一起左移
240
- const hintRight = (arrowGutter > 0 ? 4 : 6) + verticalShrink;
331
+ // 竖屏:箭头贴着可视区右缘、压在第 5 项露出的那一角上(改动前就是这个观感)。
332
+ // 箭头右缘距可视区右缘 4px => right = 可用宽 - 可视宽 + 4
333
+ // 可视宽是“前 4 + 34 露出区”,箭头宽 26,因此箭头左缘落在第 4 项之后 4px,碰不到它。
334
+ // 横屏维持原来的 6px
335
+ const hintRight = (() => {
336
+ if (!isVertical) return 6;
337
+ if (!(availableWidth > 0) || !(scrollViewWidth > 0)) return 4;
338
+ return Math.max(availableWidth - scrollViewWidth + 4, 4);
339
+ })();
340
+
341
+ // 真机核对分档结果用,确认无误后可摘掉
342
+ useEffect(() => {
343
+ if (!isVertical) return;
344
+ console.log('[ipc-player] 左下角分档', {
345
+ 个数: measuredCount,
346
+ 档位: isFlat ? 'flat 平铺' : isCompact ? 'compact 紧凑' : 'scroll 滚动',
347
+ 可用宽: availableWidth,
348
+ 内容宽: naturalWidth,
349
+ 可视宽: verticalScrollWidth || availableWidth,
350
+ 逐项宽: itemWidths
351
+ });
352
+ }, [isVertical, measuredCount, isFlat, isCompact, availableWidth, naturalWidth, verticalScrollWidth, itemWidths]);
241
353
  return /*#__PURE__*/React.createElement(CoverView, {
242
- className: clsx('ipc-player-bottom-left-content-wrap'),
354
+ className: clsx('ipc-player-bottom-left-content-wrap', wrapCls),
243
355
  style: {
244
356
  right: rightCss,
245
357
  height: showBadge ? `${coverHeight + 30}px` : `${coverHeight}px`,
@@ -295,13 +407,15 @@ const BottomLeftContent = _ref => {
295
407
  }
296
408
  },
297
409
  className: clsx(scrollCls, {
298
- 'ipc-player-bottom-left-scroll-fademask': isOverflow && !atEnd
410
+ 'ipc-player-bottom-left-scroll-fademask': fadeRightEdge,
411
+ // 锁宽档可视区只有“前 4 项 + 一角”,渐变区要跟着收窄,否则会把第 4 项也淡掉一大半
412
+ 'ipc-player-bottom-left-scroll-fademask--narrow': fadeRightEdge && verticalScrollWidth > 0
299
413
  }),
300
414
  style: {
301
415
  paddingBottom: `${paddingBottomPx}px`,
302
416
  paddingLeft: `${paddingLeftPx}px`,
303
417
  paddingRight: `${paddingRightPx}px`,
304
- width: scrollShrink > 0 ? `calc(100% - ${scrollShrink}px)` : '100%',
418
+ width: verticalScrollWidth > 0 ? `${verticalScrollWidth}px` : '100%',
305
419
  height: `${containerHeight}px`,
306
420
  marginTop: showBadge ? '-2px' : 0,
307
421
  whiteSpace: 'nowrap'
@@ -322,7 +436,7 @@ const BottomLeftContent = _ref => {
322
436
  height: '1px',
323
437
  flexShrink: 0
324
438
  }
325
- }))), isOverflow && !atEnd && (isButtonHint || !isDismissed) && /*#__PURE__*/React.createElement(CoverView, {
439
+ }))), isOverflow && !atEnd && !isCompact && (!isVertical || verticalScrollWidth > 0) && (isButtonHint || !isDismissed) && /*#__PURE__*/React.createElement(CoverView, {
326
440
  className: clsx('ipc-player-bottom-left-scroll-hint', {
327
441
  'ipc-player-bottom-left-scroll-hint--button': isButtonHint
328
442
  }),
@@ -9,6 +9,11 @@ type Props = {
9
9
  * - string: 视为合法的 CSS width 值(例如 "35%" / "20vw")
10
10
  */
11
11
  width?: number | string;
12
+ /**
13
+ * 右下角实际渲染宽度的上报回调(竖屏用)。
14
+ * 左下角据此决定自己能摆多宽 —— 右边挂几个功能都能自动让位,不再靠固定常量拍脑袋。
15
+ */
16
+ onWidthChange?: (width: number) => void;
12
17
  };
13
- declare const BottomRightContent: ({ ctx, children, width }: Props) => React.JSX.Element;
18
+ declare const BottomRightContent: ({ ctx, children, width, onWidthChange }: Props) => React.JSX.Element;
14
19
  export default BottomRightContent;
@@ -1,22 +1,60 @@
1
1
  import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
2
- import React from 'react';
2
+ import React, { useEffect, useRef } from 'react';
3
3
  import { CoverView, View } from '@ray-js/ray';
4
4
  import clsx from 'clsx';
5
5
  import { useStore } from '../ctx/store';
6
6
  import { useComponentHideState } from './hooks';
7
+
8
+ // 用唯一 className 在 createSelectorQuery 中区分多个实例,避免互相干扰
9
+ let rightClsSeq = 0;
7
10
  const BottomRightContent = _ref => {
8
11
  let {
9
12
  ctx,
10
13
  children,
11
- width
14
+ width,
15
+ onWidthChange
12
16
  } = _ref;
13
17
  const {
14
- screenType
18
+ screenType,
19
+ playState,
20
+ bottomRightContent
15
21
  } = useStore({
16
22
  screenType: ctx.screenType,
17
- playState: ctx.playState
23
+ playState: ctx.playState,
24
+ bottomRightContent: ctx.bottomRightContent
18
25
  });
19
26
  const [shouldHide] = useComponentHideState();
27
+ const idRef = useRef(++rightClsSeq);
28
+ const wrapCls = `ipc-br-wrap-${idRef.current}`;
29
+ const lastWidthRef = useRef(0);
30
+
31
+ // 实测自身宽度并上报。按钮是异步 / 按 dp 条件渲染的,宽度会在首测之后才长大,
32
+ // 因此和左下角一样在时间窗内轮询,值没变时不重复上报。
33
+ useEffect(() => {
34
+ let timer;
35
+ let attempts = 0;
36
+ const measure = () => {
37
+ const query = ty.createSelectorQuery();
38
+ query.select(`.${wrapCls}`).boundingClientRect();
39
+ query.exec(res => {
40
+ var _res$;
41
+ const w = (res === null || res === void 0 || (_res$ = res[0]) === null || _res$ === void 0 ? void 0 : _res$.width) || 0;
42
+ if (w > 0 && Math.abs(w - lastWidthRef.current) >= 1) {
43
+ lastWidthRef.current = w;
44
+ onWidthChange === null || onWidthChange === void 0 || onWidthChange(w);
45
+ }
46
+ attempts += 1;
47
+ if (attempts >= 15) {
48
+ return;
49
+ }
50
+ timer = setTimeout(measure, 400);
51
+ });
52
+ };
53
+ timer = setTimeout(measure, 60);
54
+ return () => {
55
+ if (timer) clearTimeout(timer);
56
+ };
57
+ }, [screenType, playState, bottomRightContent === null || bottomRightContent === void 0 ? void 0 : bottomRightContent.length, shouldHide, wrapCls, onWidthChange]);
20
58
  const containerHeight = screenType === 'vertical' ? 48 : 58;
21
59
 
22
60
  // 将 width 统一规范成 CSS 合法值。number → "${n}px",string → 原样透传。
@@ -29,7 +67,7 @@ const BottomRightContent = _ref => {
29
67
  boxSizing: 'border-box'
30
68
  } : null;
31
69
  return /*#__PURE__*/React.createElement(CoverView, {
32
- className: clsx('ipc-player-bottom-right-content-wrap'),
70
+ className: clsx('ipc-player-bottom-right-content-wrap', wrapCls),
33
71
  style: _objectSpread({
34
72
  paddingLeft: screenType === 'vertical' ? '10px' : '0',
35
73
  height: screenType === 'vertical' ? shouldHide ? '49px' : '48px' : shouldHide ? '59px' : '58px'
package/lib/ui/ui.js CHANGED
@@ -57,6 +57,9 @@ const SET_SCALE_ALIGN_TIMEOUT = 1500;
57
57
 
58
58
  /** 回读值与对齐值 / 目标值的比对容差 */
59
59
  const SCALE_MATCH_EPS = 0.01;
60
+
61
+ /** 竖屏左下角内容与右下角按钮之间的安全间距(px) */
62
+ const BOTTOM_RIGHT_SAFE_GAP = 12;
60
63
  export const IPCPlayerIntegration = /*#__PURE__*/React.memo(props => {
61
64
  const {
62
65
  className,
@@ -761,7 +764,7 @@ export const IPCPlayerIntegration = /*#__PURE__*/React.memo(props => {
761
764
  }, [bottomRightContent, instance]);
762
765
 
763
766
  /**
764
- * 右下角可见按钮数 → 右下角组件宽度(左下角同时用作右侧预留宽度)
767
+ * 右下角可见按钮数 → 右下角组件宽度的兜底估算值(实测拿不到时才用)
765
768
  * - 横屏: 固定 40%(用百分比,左下角自动按 100% - 40% = 60% 可视,溢出走横向滚动)
766
769
  * - 竖屏: 1 个按钮 115px / 2+ 个按钮 132px / 0 个按钮 0
767
770
  */
@@ -775,6 +778,43 @@ export const IPCPlayerIntegration = /*#__PURE__*/React.memo(props => {
775
778
  return 132;
776
779
  }, [bottomRightContent, screenType]);
777
780
 
781
+ // 右下角实测宽度(竖屏)。按钮数量 / 是否渲染都是动态的,固定常量会白白吃掉左下角可用宽,
782
+ // 这里由 BottomRightContent 实测上报,右边挂几个功能左边就自动让多少位。
783
+ const [bottomRightMeasuredWidth, setBottomRightMeasuredWidth] = useState(0);
784
+ const handleBottomRightWidthChange = useMemoizedFn(w => {
785
+ setBottomRightMeasuredWidth(prev => Math.abs(prev - w) >= 1 ? w : prev);
786
+ });
787
+
788
+ // 屏幕宽只用于给实测值封顶,取一次即可
789
+ const windowWidthRef = useRef(0);
790
+ if (!windowWidthRef.current) {
791
+ var _getSystemInfoSync;
792
+ windowWidthRef.current = ((_getSystemInfoSync = getSystemInfoSync()) === null || _getSystemInfoSync === void 0 ? void 0 : _getSystemInfoSync.windowWidth) || 0;
793
+ }
794
+
795
+ /**
796
+ * 左下角右侧要预留的宽度:竖屏优先用实测值 + 安全间距,实测不到再退回常量估算;
797
+ * 横屏保持原来的 40% 不变。
798
+ */
799
+ const bottomRightReserved = useMemo(() => {
800
+ if (screenType === 'full') {
801
+ return bottomRightWidth;
802
+ }
803
+ // 封顶保护:个别宿主里不设宽度的 CoverView 可能撑满整行,实测值会异常放大,
804
+ // 那样左下角会被挤没。超过屏宽一半就当测量不可信,退回常量估算。
805
+ const maxReserved = windowWidthRef.current ? windowWidthRef.current / 2 : 0;
806
+ if (bottomRightMeasuredWidth > 0 && (!maxReserved || bottomRightMeasuredWidth <= maxReserved)) {
807
+ return Math.ceil(bottomRightMeasuredWidth) + BOTTOM_RIGHT_SAFE_GAP;
808
+ }
809
+ return bottomRightWidth;
810
+ }, [screenType, bottomRightWidth, bottomRightMeasuredWidth]);
811
+
812
+ /**
813
+ * 传给右下角自身的 width:竖屏不再强制固定宽(否则测出来的永远是那个常量),
814
+ * 让它按内容自然收缩;横屏维持 40%。
815
+ */
816
+ const bottomRightStyleWidth = screenType === 'full' ? bottomRightWidth : undefined;
817
+
778
818
  /**
779
819
  * 视频流加载状态封装
780
820
  */
@@ -872,7 +912,20 @@ export const IPCPlayerIntegration = /*#__PURE__*/React.memo(props => {
872
912
  const onChangeIgnoreHideStopPreview = state => {
873
913
  setIgnoreStopOnHide(state);
874
914
  };
875
- const onSetScaleMultiple = value => {
915
+ const onSetScaleMultiple = payload => {
916
+ // 载荷兼容两种形态:数字(一次性落地,走对齐流程)、
917
+ // { value, immediate }(转盘拖动中的连续下发,跳过对齐)
918
+ const value = typeof payload === 'number' ? payload : payload === null || payload === void 0 ? void 0 : payload.value;
919
+ const immediate = typeof payload === 'object' && !!(payload !== null && payload !== void 0 && payload.immediate);
920
+ if (!(value > 0)) return;
921
+
922
+ // 连续下发:属性每次都在变,播放器一定收得到,不需要中间态;
923
+ // 反而插一帧 actual 会让画面在拖动中抖一下。
924
+ if (immediate) {
925
+ applyScaleMultiple(value);
926
+ return;
927
+ }
928
+
876
929
  // 手势缩放只走 onZoomChange 回读、不写本 state,属性会停在旧值(甚至恰好等于目标值),
877
930
  // 直接设目标时「值没变化」,播放器收不到。
878
931
  // 故下发前先把属性对齐画面实际值 currentZoomLevel,再设目标值,保证属性确实变化一次。
@@ -1120,10 +1173,11 @@ export const IPCPlayerIntegration = /*#__PURE__*/React.memo(props => {
1120
1173
  }
1121
1174
  }, /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(BottomLeftContent, {
1122
1175
  ctx: instance,
1123
- reservedRight: bottomRightWidth
1176
+ reservedRight: bottomRightReserved
1124
1177
  }, renderBottomLeftContent), /*#__PURE__*/React.createElement(BottomRightContent, {
1125
1178
  ctx: instance,
1126
- width: bottomRightWidth
1179
+ width: bottomRightStyleWidth,
1180
+ onWidthChange: handleBottomRightWidthChange
1127
1181
  }, renderBottomRightContent))), (() => {
1128
1182
  if (!absoluteContent || !(absoluteContent !== null && absoluteContent !== void 0 && absoluteContent.length)) return null;
1129
1183
  return absoluteContent.map(item => {
package/lib/ui/ui.less CHANGED
@@ -235,6 +235,23 @@
235
235
  );
236
236
  }
237
237
 
238
+ // 竖屏滚动档:可视区被收窄到“前 4 项 + 露一角”,渐变区同步收窄到只覆盖那一角,
239
+ // 否则 72px 的渐变会把第 4 项也淡掉一大半
240
+ .ipc-player-bottom-left-scroll-fademask--narrow {
241
+ -webkit-mask-image: linear-gradient(
242
+ to right,
243
+ #000 calc(100% - 34px),
244
+ rgba(0, 0, 0, 0.3) calc(100% - 14px),
245
+ transparent 100%
246
+ );
247
+ mask-image: linear-gradient(
248
+ to right,
249
+ #000 calc(100% - 34px),
250
+ rgba(0, 0, 0, 0.3) calc(100% - 14px),
251
+ transparent 100%
252
+ );
253
+ }
254
+
238
255
  @keyframes ipc-player-bottom-left-scroll-hint-pulse {
239
256
  0%,
240
257
  100% {
@@ -2,6 +2,7 @@ import React, { useMemo } from 'react';
2
2
  import { View, Text } from '@ray-js/ray';
3
3
  import clsx from 'clsx';
4
4
  import { useStore } from '../../ctx/store';
5
+ import { PowerMode } from '../../ports.output';
5
6
  import './battery.less';
6
7
  export const createBattery = _ref => {
7
8
  let {
@@ -19,8 +20,13 @@ export const createBattery = _ref => {
19
20
  });
20
21
  const {
21
22
  batteryValue,
22
- batteryCharging
23
+ batteryCharging,
24
+ powerMode,
25
+ solarCharging
23
26
  } = useBattery(devId);
27
+
28
+ // powerMode 缺省时(外部注入的 useBattery 未透传)回退到 batteryCharging 判断
29
+ const isSolar = powerMode ? powerMode === PowerMode.Solar : !!solarCharging;
24
30
  const batteryColor = useMemo(() => {
25
31
  if (!batteryValue) return '#eb3223';
26
32
  if (batteryValue >= 60) {
@@ -38,7 +44,7 @@ export const createBattery = _ref => {
38
44
  className: clsx(className)
39
45
  }, /*#__PURE__*/React.createElement(View, {
40
46
  className: "ipc-player-plugin-battery"
41
- }, batteryCharging ? /*#__PURE__*/React.createElement(View, {
47
+ }, !isSolar && batteryCharging ? /*#__PURE__*/React.createElement(View, {
42
48
  className: "icon-panel icon-panel-charging ipc-player-plugin-battery-charging"
43
49
  }) : /*#__PURE__*/React.createElement(View, {
44
50
  className: "ipc-player-plugin-battery-border"
@@ -52,7 +58,9 @@ export const createBattery = _ref => {
52
58
  backgroundColor: batteryColor
53
59
  },
54
60
  className: "ipc-player-plugin-battery-value-value"
55
- }))), /*#__PURE__*/React.createElement(Text, {
61
+ })), isSolar && /*#__PURE__*/React.createElement(View, {
62
+ className: "icon-panel icon-panel-solar ipc-player-plugin-battery-solar"
63
+ })), /*#__PURE__*/React.createElement(Text, {
56
64
  className: "ipc-player-plugin-battery-text"
57
65
  }, batteryValue, "%")));
58
66
  });
@@ -44,6 +44,17 @@
44
44
  border-radius: 1rpx;
45
45
  }
46
46
  }
47
+ /* 太阳能充电:叠在电池正中央,压在电量条之上 */
48
+ .ipc-player-plugin-battery-solar {
49
+ position: absolute;
50
+ left: 50%;
51
+ top: 50%;
52
+ transform: translate(-50%, -50%);
53
+ z-index: 1;
54
+ font-size: calc(8px * var(--ipc-player-size-scale, 1));
55
+ line-height: 1;
56
+ color: #fff;
57
+ }
47
58
  }
48
59
  }
49
60
 
@@ -2,6 +2,7 @@ import React, { useMemo } from 'react';
2
2
  import { View, Text } from '@ray-js/ray';
3
3
  import clsx from 'clsx';
4
4
  import { useStore } from '../../ctx/store';
5
+ import { PowerMode } from '../../ports.output';
5
6
  import './battery.less';
6
7
  export const createBattery = _ref => {
7
8
  let {
@@ -19,8 +20,13 @@ export const createBattery = _ref => {
19
20
  });
20
21
  const {
21
22
  batteryValue,
22
- batteryCharging
23
+ batteryCharging,
24
+ powerMode,
25
+ solarCharging
23
26
  } = useBattery(devId);
27
+
28
+ // powerMode 缺省时(外部注入的 useBattery 未透传)回退到 batteryCharging 判断
29
+ const isSolar = powerMode ? powerMode === PowerMode.Solar : !!solarCharging;
24
30
  const batteryColor = useMemo(() => {
25
31
  if (!batteryValue) return '#eb3223';
26
32
  if (batteryValue >= 60) {
@@ -38,7 +44,7 @@ export const createBattery = _ref => {
38
44
  className: clsx(className)
39
45
  }, /*#__PURE__*/React.createElement(View, {
40
46
  className: "ipc-player-plugin-battery"
41
- }, batteryCharging ? /*#__PURE__*/React.createElement(View, {
47
+ }, !isSolar && batteryCharging ? /*#__PURE__*/React.createElement(View, {
42
48
  className: "icon-panel icon-panel-charging ipc-player-plugin-battery-charging"
43
49
  }) : /*#__PURE__*/React.createElement(View, {
44
50
  className: "ipc-player-plugin-battery-border"
@@ -52,7 +58,9 @@ export const createBattery = _ref => {
52
58
  backgroundColor: batteryColor
53
59
  },
54
60
  className: "ipc-player-plugin-battery-value-value"
55
- }))), /*#__PURE__*/React.createElement(Text, {
61
+ })), isSolar && /*#__PURE__*/React.createElement(View, {
62
+ className: "icon-panel icon-panel-solar ipc-player-plugin-battery-solar"
63
+ })), /*#__PURE__*/React.createElement(Text, {
56
64
  className: "ipc-player-plugin-battery-text"
57
65
  }, batteryValue, "%")));
58
66
  });