@ray-js/ipc-player-integration 0.0.35-beta.30 → 0.0.35-beta.31

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.
@@ -24,7 +24,7 @@ export async function initPlayerWidgets(ctx, options) {
24
24
  var _options$hideSmartIma;
25
25
  // @ts-ignore
26
26
  newDefaultBottomLeftContent[tryExperienceIndex].initProps = {
27
- hideTryExperienceMenu: (_options$hideSmartIma = options.hideSmartImageQualityState) !== null && _options$hideSmartIma !== void 0 ? _options$hideSmartIma : true
27
+ hideSmartImageQualityState: (_options$hideSmartIma = options.hideSmartImageQualityState) !== null && _options$hideSmartIma !== void 0 ? _options$hideSmartIma : true
28
28
  };
29
29
  }
30
30
  if (resolutionIndex !== -1) {
@@ -10,6 +10,12 @@ type Props = {
10
10
  */
11
11
  reservedRight?: number | string;
12
12
  canOpenSettings?: boolean;
13
+ /**
14
+ * 横滑提示箭头的交互模式:
15
+ * - 'button': 白色小圆底 + 黑色箭头,溢出后常显、可点击滑动(不随拖动消失)
16
+ * - 'pulse' : 旧版渐隐渐显提示箭头,拖动一次后当前屏幕模式不再展示
17
+ */
18
+ scrollHintMode?: 'button' | 'pulse';
13
19
  };
14
- declare const BottomLeftContent: ({ ctx, children, reservedRight, canOpenSettings }: Props) => React.JSX.Element;
20
+ declare const BottomLeftContent: ({ ctx, children, reservedRight, canOpenSettings, scrollHintMode, }: Props) => React.JSX.Element;
15
21
  export default BottomLeftContent;
@@ -1,31 +1,27 @@
1
+ import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
1
2
  import "core-js/modules/esnext.iterator.constructor.js";
2
3
  import "core-js/modules/esnext.iterator.find.js";
3
- import React, { useMemo } from 'react';
4
- import { CoverView, View } from '@ray-js/ray';
4
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
5
+ import { CoverView, ScrollView, View } from '@ray-js/ray';
5
6
  import clsx from 'clsx';
6
7
  import { useStore } from '../ctx/store';
7
8
  import { useComponentHideState } from './hooks';
8
9
  import { TrialBadge, useTrialBadge } from '../widgets/trialBadge';
9
- /**
10
- * 右侧透明渐变 mask,用于提示横向可滑动。
11
- * - 竖屏容器窄,48px 渐变区已足够明显
12
- * - 横屏容器约占 65% 宽度(≥ 500px),且单个按钮就有 ~72px,
13
- * 渐变区需大于一个按钮宽度才能让最右侧按钮"半隐半现",
14
- * 暗示后面还有内容
15
- */
16
- const getFadeMask = screenType => {
17
- const fadeWidth = screenType === 'vertical' ? 60 : 120;
18
- return `linear-gradient(to right, #000 calc(100% - ${fadeWidth}px), transparent 100%)`;
19
- };
10
+ import { pauseTimeToHideAllComponent, startTimeToHideAllComponent } from './constant';
11
+
12
+ // 用唯一 className createSelectorQuery 中区分多个实例,避免互相干扰
13
+ let scrollClsSeq = 0;
20
14
  const BottomLeftContent = _ref => {
21
15
  let {
22
16
  ctx,
23
17
  children,
24
18
  reservedRight = 0,
25
- canOpenSettings = true
19
+ canOpenSettings = true,
20
+ scrollHintMode = 'button'
26
21
  } = _ref;
27
22
  const {
28
23
  screenType,
24
+ playState,
29
25
  brandColor,
30
26
  bottomLeftContent
31
27
  } = useStore({
@@ -37,7 +33,7 @@ const BottomLeftContent = _ref => {
37
33
  const showSmartImageQuality = useMemo(() => {
38
34
  var _tryExp$initProps;
39
35
  const tryExp = bottomLeftContent === null || bottomLeftContent === void 0 ? void 0 : bottomLeftContent.find(item => item.id === 'TryExperience');
40
- return !(tryExp !== null && tryExp !== void 0 && (_tryExp$initProps = tryExp.initProps) !== null && _tryExp$initProps !== void 0 && _tryExp$initProps.hideTryExperienceMenu);
36
+ return !(tryExp !== null && tryExp !== void 0 && (_tryExp$initProps = tryExp.initProps) !== null && _tryExp$initProps !== void 0 && _tryExp$initProps.hideSmartImageQualityState);
41
37
  }, [bottomLeftContent]);
42
38
  const trialRemainingSec = useMemo(() => {
43
39
  var _trialRemainingSec, _tryExp$initProps2;
@@ -65,14 +61,155 @@ const BottomLeftContent = _ref => {
65
61
  handleTrialSubscribe,
66
62
  handleCountdownEnd
67
63
  } = useTrialBadge(ctx.event, ctx.devId, trialRemainingSec);
64
+
65
+ // 横向滚动状态:内容溢出且未滑到对应边缘时,显示左 / 右箭头提示
66
+ const idRef = useRef(++scrollClsSeq);
67
+ const scrollCls = `ipc-bl-scroll-${idRef.current}`;
68
+ const contentCls = `ipc-bl-scroll-content-${idRef.current}`;
69
+ // 内容末尾哨兵节点:用它的真实横坐标反推子项总宽,避免内容容器被撑满导致测不出溢出
70
+ const endCls = `ipc-bl-scroll-end-${idRef.current}`;
71
+ const [scrollLeft, setScrollLeft] = useState(0);
72
+ const [clientWidth, setClientWidth] = useState(0);
73
+ const [contentWidth, setContentWidth] = useState(0);
74
+ // 点击箭头时的受控滚动目标;仅在程序化滚动期间为数值,结束后置回 undefined,
75
+ // 避免持续受控导致用户手动拖动被回弹
76
+ const [scrollTo, setScrollTo] = useState(undefined);
77
+ // 横竖屏切换后,强制 ScrollView 重新挂载并把滚动状态重置为初始值
78
+ const [scrollResetKey, setScrollResetKey] = useState(0);
79
+ // 用户在某种屏幕模式下拖动过一次之后,该模式不再展示箭头(竖屏 / 横屏各自独立记录)
80
+ const [dismissed, setDismissed] = useState({
81
+ vertical: false,
82
+ full: false
83
+ });
84
+
85
+ // 滑动期间暂停整体的自动隐藏,滑动停止后再恢复(无原生 scrollend,用防抖判定结束)
86
+ const scrollingRef = useRef(false);
87
+ const scrollEndTimerRef = useRef();
88
+ // 测量时需要叠加当前滚动量,用 ref 避免闭包拿到过期值
89
+ const scrollLeftRef = useRef(0);
90
+ useEffect(() => {
91
+ setScrollResetKey(k => k + 1);
92
+ setScrollLeft(0);
93
+ setScrollTo(undefined);
94
+ }, [screenType]);
95
+
96
+ // 程序化滚动目标只在动画期间短暂受控,之后释放为非受控,避免回弹影响手动拖动
97
+ useEffect(() => {
98
+ if (scrollTo === undefined) {
99
+ return undefined;
100
+ }
101
+ const t = setTimeout(() => setScrollTo(undefined), 600);
102
+ return () => clearTimeout(t);
103
+ }, [scrollTo]);
68
104
  const paddingLeftPx = screenType === 'vertical' ? 0 : 25;
105
+ // 滚动末端的安全距离:滑到最右时最后一个元素与右边缘留更宽松的间距(横屏更宽)
106
+ const paddingRightPx = screenType === 'vertical' ? 22 : 32;
107
+ const paddingBottomPx = screenType === 'vertical' ? 0 : 25;
69
108
  const showBadge = showSmartImageQuality && showTrialBadge && canOpenSettings && gRawData && !isLowPhone && buttonState === 1 && trialRemainingSec > 0;
109
+
110
+ // 测量滚动容器可视宽度与内部内容宽度。
111
+ // 首屏漏判溢出的根因:左下角子元素(画质菜单 / 试用角标等)是异步加载的,宽度会在
112
+ // 首次测量之后才“长大”,而固定几次延时跑完就不再测,导致 contentWidth 偏小、判不出溢出。
113
+ // 这里在一段时间窗内持续轮询(每 400ms 测一次,约 6s),每次都用最新读数更新;
114
+ // 值未变化时 React 会自动跳过重渲染,因此持续轮询不会带来额外开销,
115
+ // 同时能兜住异步内容撑开与 overlay 布局滞后导致的尺寸变化。
116
+ useEffect(() => {
117
+ let timer;
118
+ let attempts = 0;
119
+ const measure = () => {
120
+ const query = ty.createSelectorQuery();
121
+ query.select(`.${scrollCls}`).boundingClientRect();
122
+ query.select(`.${contentCls}`).boundingClientRect();
123
+ query.select(`.${endCls}`).boundingClientRect();
124
+ query.exec(res => {
125
+ const scrollRect = res === null || res === void 0 ? void 0 : res[0];
126
+ const contentRect = res === null || res === void 0 ? void 0 : res[1];
127
+ const endRect = res === null || res === void 0 ? void 0 : res[2];
128
+ const cw = (scrollRect === null || scrollRect === void 0 ? void 0 : scrollRect.width) || 0;
129
+ if (cw) {
130
+ setClientWidth(cw);
131
+ }
132
+
133
+ // 优先用末尾哨兵反推子项真实总宽:
134
+ // 哨兵视口 x = 滚动容器左 + 左padding + 子项总宽 - 当前 scrollLeft
135
+ // => 子项总宽 = 哨兵.left - 滚动容器.left - 左padding + scrollLeft
136
+ // 这样即使内容容器被撑满到可视内盒宽,也能拿到真实内容宽;哨兵不可用时回退到容器宽。
137
+ let tw = 0;
138
+ if (scrollRect && endRect && typeof endRect.left === 'number') {
139
+ tw = endRect.left - scrollRect.left - paddingLeftPx + scrollLeftRef.current;
140
+ }
141
+ if (!(tw > 0)) {
142
+ tw = (contentRect === null || contentRect === void 0 ? void 0 : contentRect.width) || 0;
143
+ }
144
+ if (tw > 0) {
145
+ setContentWidth(tw);
146
+ }
147
+ attempts += 1;
148
+ // 最多轮询 15 次(约 6s)兜底,避免无限轮询
149
+ if (attempts >= 15) {
150
+ return;
151
+ }
152
+ timer = setTimeout(measure, 400);
153
+ });
154
+ };
155
+ timer = setTimeout(measure, 200);
156
+ return () => {
157
+ if (timer) clearTimeout(timer);
158
+ };
159
+ }, [screenType,
160
+ // reservedRight 决定 ScrollView 可视宽度(右下角内容异步加载后会从 0 变为实际宽度)
161
+ reservedRight,
162
+ // playState:视频就绪后 overlay 可能重新布局,需重新测量
163
+ playState, bottomLeftContent === null || bottomLeftContent === void 0 ? void 0 : bottomLeftContent.length, shouldHide, showBadge, scrollCls, contentCls, scrollResetKey]);
164
+ // 卸载时清理防抖定时器;若仍处于滑动中则恢复自动隐藏,避免遗留暂停态
165
+ useEffect(() => {
166
+ return () => {
167
+ if (scrollEndTimerRef.current) {
168
+ clearTimeout(scrollEndTimerRef.current);
169
+ }
170
+ if (scrollingRef.current) {
171
+ var _ctx$event3;
172
+ scrollingRef.current = false;
173
+ (_ctx$event3 = ctx.event) === null || _ctx$event3 === void 0 || _ctx$event3.emit(startTimeToHideAllComponent);
174
+ }
175
+ };
176
+ }, [ctx.event]);
177
+
178
+ // ScrollView 的实际可滚动宽度 = 左右 padding + 子元素总宽
179
+ const totalContentWidth = contentWidth > 0 ? paddingLeftPx + contentWidth + paddingRightPx : 0;
180
+ const isOverflow = totalContentWidth > clientWidth + 1;
181
+ const atEnd = !isOverflow || scrollLeft + clientWidth >= totalContentWidth - 2;
182
+ const isDismissed = dismissed[screenType];
183
+ const isButtonHint = scrollHintMode === 'button';
184
+ // button 模式:溢出且未到末端即常显(不受 dismissed 影响);pulse 模式:保留拖动后消失的旧逻辑
185
+
186
+ // 右侧还有未滚到头的内容时,启用右边缘淡出,把没完全露出的元素渐隐隐藏(横竖屏都生效)
187
+
188
+ // 点击右箭头:直接滚动到最右端。
189
+ // 给一个足够大的目标值,交由 ScrollView 自身夹取到真实最右端,避免测量误差导致滚不到底。
190
+
70
191
  const coverHeight = screenType === 'vertical' ? shouldHide ? 49 : 48 : shouldHide ? 84 : 83;
71
192
  const containerHeight = screenType === 'vertical' ? 48 : 58;
72
193
 
73
194
  // reservedRight 兼容 number(px)与 string(任意 CSS 长度,例如 "35%")
74
195
  const rightCss = typeof reservedRight === 'number' ? `${reservedRight}px` : reservedRight;
75
- const fadeMask = getFadeMask(screenType);
196
+
197
+ // 与 bottomRightContent 对齐:bottomRight 的 icon 行是贴在 wrap 顶部(不居中)的,
198
+ // 这里 wrap 用 flex-start,让 ScrollView 也贴顶;icon 在 ScrollView 内通过 align-items
199
+ // 居中到可视行中线,从而和右侧 icon 在同一水平线上。
200
+ // hintBottom 锚定 wrap 底部:
201
+ // icon 中心到 wrap 底部 = (wrap底部→SV底部空隙) + SV的paddingBottom + 可视行高/2
202
+ // 竖屏: 0 + 0 + 24 = 24,箭头 16 → bottom = 16
203
+ // 横屏: 25 + 25 + 16.5 = 66.5,箭头 16 → bottom = 58
204
+
205
+ const hintBottom = screenType === 'vertical' ? 16 : 58;
206
+ // button 模式用半透明圆做点击热区,比 pulse 提示大;下移半个差值以保持图标中心不变
207
+ const hintSize = isButtonHint ? 26 : 16;
208
+ // 竖屏 button 模式给 ScrollView 右侧留一条较窄的“箭头槽”:只裁掉内容最右一小段,
209
+ // 箭头压住最后一个元素的少量边缘(不完全遮挡),整体更贴近内容
210
+ const arrowGutter = isButtonHint && screenType === 'vertical' ? 12 : 0;
211
+ // 箭头贴右边缘放置(竖屏 4px / 横屏 6px),不再随 gutter 居中
212
+ const hintRight = arrowGutter > 0 ? 4 : 6;
76
213
  return /*#__PURE__*/React.createElement(CoverView, {
77
214
  className: clsx('ipc-player-bottom-left-content-wrap'),
78
215
  style: {
@@ -80,7 +217,7 @@ const BottomLeftContent = _ref => {
80
217
  height: showBadge ? `${coverHeight + 30}px` : `${coverHeight}px`,
81
218
  display: 'flex',
82
219
  flexDirection: 'column',
83
- justifyContent: 'center',
220
+ justifyContent: 'flex-start',
84
221
  opacity: shouldHide ? 0 : 1,
85
222
  pointerEvents: shouldHide ? 'none' : 'auto'
86
223
  }
@@ -94,22 +231,102 @@ const BottomLeftContent = _ref => {
94
231
  trialRemainingSec: trialRemainingSec,
95
232
  onSubscribe: handleTrialSubscribe,
96
233
  onCountdownEnd: handleCountdownEnd
97
- })), /*#__PURE__*/React.createElement(View, {
234
+ })), /*#__PURE__*/React.createElement(ScrollView, {
235
+ key: scrollResetKey,
236
+ scrollX: true,
237
+ scrollLeft: scrollTo,
238
+ scrollWithAnimation: true,
239
+ onScroll: e => {
240
+ var _e$detail;
241
+ // 滑动开始时暂停自动隐藏(仅在一次滑动会话内触发一次),滑动停止后恢复
242
+ if (!scrollingRef.current) {
243
+ var _ctx$event;
244
+ scrollingRef.current = true;
245
+ (_ctx$event = ctx.event) === null || _ctx$event === void 0 || _ctx$event.emit(pauseTimeToHideAllComponent);
246
+ }
247
+ if (scrollEndTimerRef.current) {
248
+ clearTimeout(scrollEndTimerRef.current);
249
+ }
250
+ scrollEndTimerRef.current = setTimeout(() => {
251
+ var _ctx$event2;
252
+ scrollingRef.current = false;
253
+ (_ctx$event2 = ctx.event) === null || _ctx$event2 === void 0 || _ctx$event2.emit(startTimeToHideAllComponent);
254
+ }, 200);
255
+
256
+ // Ray ScrollView 在不同实现下,scrollLeft 可能直接挂在事件上,也可能在 detail 内
257
+ const sl = typeof (e === null || e === void 0 ? void 0 : e.scrollLeft) === 'number' ? e.scrollLeft : e === null || e === void 0 || (_e$detail = e.detail) === null || _e$detail === void 0 ? void 0 : _e$detail.scrollLeft;
258
+ if (typeof sl === 'number') {
259
+ scrollLeftRef.current = sl;
260
+ setScrollLeft(sl);
261
+ // 一旦真的拖动过(>2px 阈值过滤抖动 / 重置触发的 0 值),当前屏幕模式下不再展示箭头
262
+ if (sl > 2 && !dismissed[screenType]) {
263
+ setDismissed(d => _objectSpread(_objectSpread({}, d), {}, {
264
+ [screenType]: true
265
+ }));
266
+ }
267
+ }
268
+ },
269
+ className: clsx(scrollCls, {
270
+ 'ipc-player-bottom-left-scroll-fademask': isOverflow && !atEnd
271
+ }),
98
272
  style: {
99
- paddingBottom: screenType === 'vertical' ? 0 : '25px',
273
+ paddingBottom: `${paddingBottomPx}px`,
100
274
  paddingLeft: `${paddingLeftPx}px`,
101
- paddingRight: '16px',
102
- width: '100%',
275
+ paddingRight: `${paddingRightPx}px`,
276
+ width: arrowGutter > 0 ? `calc(100% - ${arrowGutter}px)` : '100%',
103
277
  height: `${containerHeight}px`,
104
278
  marginTop: showBadge ? '-2px' : 0,
105
- overflowX: 'auto',
106
- overflowY: 'hidden',
107
- whiteSpace: 'nowrap',
108
- WebkitOverflowScrolling: 'touch',
109
- WebkitMaskImage: fadeMask,
110
- maskImage: fadeMask
279
+ whiteSpace: 'nowrap'
280
+ }
281
+ }, /*#__PURE__*/React.createElement(View, {
282
+ className: clsx('ipc-player-bottom-left-content-container', contentCls),
283
+ style: {
284
+ display: 'inline-flex',
285
+ flexDirection: 'row',
286
+ height: `${containerHeight - paddingBottomPx}px`,
287
+ alignItems: 'center'
288
+ }
289
+ }, children, /*#__PURE__*/React.createElement(View, {
290
+ className: endCls,
291
+ style: {
292
+ width: '1px',
293
+ height: '1px',
294
+ flexShrink: 0
295
+ }
296
+ }))), isOverflow && !atEnd && (isButtonHint || !isDismissed) && /*#__PURE__*/React.createElement(CoverView, {
297
+ className: clsx('ipc-player-bottom-left-scroll-hint', {
298
+ 'ipc-player-bottom-left-scroll-hint--button': isButtonHint
299
+ }),
300
+ style: {
301
+ position: 'absolute',
302
+ right: `${hintRight}px`,
303
+ bottom: `${hintBottom - (hintSize - 16) / 2}px`,
304
+ width: `${hintSize}px`,
305
+ height: `${hintSize}px`,
306
+ display: 'flex',
307
+ alignItems: 'center',
308
+ justifyContent: 'center',
309
+ pointerEvents: isButtonHint ? 'auto' : 'none'
111
310
  },
112
- className: clsx('ipc-player-bottom-left-content-container')
113
- }, children));
311
+ onClick: isButtonHint ? () => {
312
+ if (!isOverflow) {
313
+ return;
314
+ }
315
+ setScrollTo(totalContentWidth + clientWidth + 9999);
316
+ } : undefined
317
+ }, /*#__PURE__*/React.createElement(View, {
318
+ className: clsx('ipc-player-bottom-left-scroll-hint-arrow', {
319
+ 'ipc-player-bottom-left-scroll-hint-arrow--right': true,
320
+ 'ipc-player-bottom-left-scroll-hint-arrow--light': isButtonHint,
321
+ 'ipc-player-bottom-left-scroll-hint-arrow--static': isButtonHint
322
+ })
323
+ // 右向 chevron 由 top+right 描边旋转而成,视觉重心偏右;放大并整体左移使其在圆内居中
324
+ ,
325
+ style: isButtonHint ? {
326
+ width: '9px',
327
+ height: '9px',
328
+ transform: 'translate(-1px, 0) rotate(45deg)'
329
+ } : undefined
330
+ })));
114
331
  };
115
332
  export default BottomLeftContent;
package/lib/ui/ui.d.ts CHANGED
@@ -32,12 +32,13 @@ type Props = {
32
32
  awakeStatus?: boolean | undefined;
33
33
  eventRef?: React.RefObject<EventInstance>;
34
34
  onPlayerTap?: (data: any) => void;
35
- defaultAutoPlay?: boolean;
36
35
  limitFlow?: boolean;
37
36
  showFlowLowTip?: boolean;
38
37
  extend?: Record<string, any>;
39
38
  refreshElement?: boolean;
40
39
  autoWakeUp?: boolean;
40
+ previewEnabled?: boolean;
41
+ onPreviewEnabledChange?: (enabled: boolean) => void;
41
42
  };
42
43
  export declare const IPCPlayerIntegration: React.MemoExoticComponent<(props: Props) => React.JSX.Element>;
43
44
  export {};
package/lib/ui/ui.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import _objectSpread from "@babel/runtime/helpers/esm/objectSpread2";
2
2
  import "core-js/modules/esnext.iterator.constructor.js";
3
3
  import "core-js/modules/esnext.iterator.filter.js";
4
+ import "core-js/modules/esnext.iterator.find.js";
4
5
  import "core-js/modules/esnext.iterator.map.js";
5
6
  import React, { useContext, useState, useRef, useMemo, useEffect, useImperativeHandle } from 'react';
6
7
  import { View, CoverView, getSystemInfoSync, usePageEvent, setNavigationBarBack, setPageOrientation, getCurrentPages } from '@ray-js/ray';
@@ -64,12 +65,13 @@ export const IPCPlayerIntegration = /*#__PURE__*/React.memo(props => {
64
65
  awakeStatus = undefined,
65
66
  onPlayerTap,
66
67
  playerRoute = 'pages/home/index',
67
- defaultAutoPlay = true,
68
68
  limitFlow = false,
69
69
  showFlowLowTip = false,
70
70
  extend = {},
71
71
  refreshElement = false,
72
- autoWakeUp = false
72
+ autoWakeUp = false,
73
+ previewEnabled = undefined,
74
+ onPreviewEnabledChange
73
75
  } = props;
74
76
  const {
75
77
  event
@@ -215,7 +217,8 @@ export const IPCPlayerIntegration = /*#__PURE__*/React.memo(props => {
215
217
  return () => {
216
218
  var _ty3;
217
219
  if (typeof ((_ty3 = ty) === null || _ty3 === void 0 ? void 0 : _ty3.offOrientationChange) === 'function') {
218
- ty.offOrientationChange(handleOrientationChange);
220
+ var _ty4;
221
+ (_ty4 = ty) === null || _ty4 === void 0 || _ty4.offOrientationChange(handleOrientationChange);
219
222
  }
220
223
  };
221
224
  }, []);
@@ -444,7 +447,19 @@ export const IPCPlayerIntegration = /*#__PURE__*/React.memo(props => {
444
447
  setBrandColor(brandColor);
445
448
  setVerticalMic(verticalMic);
446
449
  }, [brandColor, verticalMic]);
450
+
451
+ // 是否隐藏画质(试用)功能:取 TryExperience widget 的 hideSmartImageQualityState
452
+ // (由 initPlayerWidgets 的 hideSmartImageQualityState ?? true 映射而来)
453
+ const hideSmartImageQualityState = useMemo(() => {
454
+ var _hideSmartImageQualit, _tryExp$initProps;
455
+ const tryExp = bottomLeftContent === null || bottomLeftContent === void 0 ? void 0 : bottomLeftContent.find(item => item.id === 'TryExperience');
456
+ return (_hideSmartImageQualit = tryExp === null || tryExp === void 0 || (_tryExp$initProps = tryExp.initProps) === null || _tryExp$initProps === void 0 ? void 0 : _tryExp$initProps.hideSmartImageQualityState) !== null && _hideSmartImageQualit !== void 0 ? _hideSmartImageQualit : true;
457
+ }, [bottomLeftContent]);
447
458
  const refreshSmartImageQuality = useMemoizedFn(async () => {
459
+ // 隐藏画质功能时直接跳过,不发起请求与更新
460
+ console.log('hideSmartImageQualityState');
461
+ if (hideSmartImageQualityState) return;
462
+ console.log('refreshSmartImageQuality');
448
463
  const res = await getSmartImageQualityState(devId);
449
464
  if (!res) return;
450
465
  // 调试时使用
@@ -465,17 +480,12 @@ export const IPCPlayerIntegration = /*#__PURE__*/React.memo(props => {
465
480
  isLowPhone: isLowPhoneRef.current
466
481
  });
467
482
  });
468
- const siqInitRef = useRef(false);
483
+
484
+ // 首次进入即执行;hideSmartImageQualityState 变化时重新执行。
485
+ // 是否隐藏的判定在 refreshSmartImageQuality 内部,hide 为 true 时方法内直接 return。
469
486
  useEffect(() => {
470
- if (playState === PlayState.PLAYING) {
471
- if (!siqInitRef.current) {
472
- siqInitRef.current = true;
473
- refreshSmartImageQuality();
474
- }
475
- } else {
476
- siqInitRef.current = false;
477
- }
478
- }, [instance, playState]);
487
+ refreshSmartImageQuality();
488
+ }, [hideSmartImageQualityState]);
479
489
  const refreshBottomLeft = () => {
480
490
  event.current.emit(startTimeToHideAllComponent);
481
491
  event.current.emit(showAllComponent);
@@ -512,6 +522,33 @@ export const IPCPlayerIntegration = /*#__PURE__*/React.memo(props => {
512
522
  setIsSmallScreen(computeIsSmallScreen());
513
523
  });
514
524
 
525
+ // 页面是否处于前台;自动隐藏倒计时要等播放器正常出流(PLAYING)后再启动
526
+ const pageShownRef = useRef(false);
527
+ // 用 ref 同步最新 playState,避免 usePageEvent 回调闭包拿到过期值
528
+ const playStateRef = useRef(playState);
529
+ playStateRef.current = playState;
530
+ usePageEvent('onShow', () => {
531
+ pageShownRef.current = true;
532
+ // 回到前台时刷新画质(试用)状态(方法内部会按 hideSmartImageQualityState 自行短路)
533
+ refreshSmartImageQuality();
534
+ // 已经在出流就直接启动;否则等 playState 变为 PLAYING 的 effect 再启动
535
+ if (playStateRef.current === PlayState.PLAYING) {
536
+ eventRef.current.emit(startTimeToHideAllComponent);
537
+ }
538
+ });
539
+ // 进入后台 停止播放器元素动画计时
540
+ usePageEvent('onHide', () => {
541
+ pageShownRef.current = false;
542
+ eventRef.current.emit(pauseTimeToHideAllComponent);
543
+ });
544
+
545
+ // 播放器正常出流后再开始自动隐藏倒计时(仅在前台时)
546
+ useEffect(() => {
547
+ if (pageShownRef.current && playState === PlayState.PLAYING) {
548
+ eventRef.current.emit(startTimeToHideAllComponent);
549
+ }
550
+ }, [playState]);
551
+
515
552
  /**
516
553
  * 监听屏幕布局变化
517
554
  */
@@ -680,12 +717,12 @@ export const IPCPlayerIntegration = /*#__PURE__*/React.memo(props => {
680
717
 
681
718
  /**
682
719
  * 右下角可见按钮数 → 右下角组件宽度(左下角同时用作右侧预留宽度)
683
- * - 横屏: 固定 35%(用百分比,左下角自动按 100% - 35% = 65% 可视,溢出走横向滚动)
720
+ * - 横屏: 固定 40%(用百分比,左下角自动按 100% - 40% = 60% 可视,溢出走横向滚动)
684
721
  * - 竖屏: 1 个按钮 115px / 2+ 个按钮 132px / 0 个按钮 0
685
722
  */
686
723
  const bottomRightWidth = useMemo(() => {
687
724
  if (screenType === 'full') {
688
- return '35%';
725
+ return '40%';
689
726
  }
690
727
  const visibleCount = (bottomRightContent || []).filter(item => !item.hidden).length;
691
728
  if (visibleCount <= 0) return 0;
@@ -979,7 +1016,8 @@ export const IPCPlayerIntegration = /*#__PURE__*/React.memo(props => {
979
1016
  ,
980
1017
  brandColor: brandColor,
981
1018
  playerRoute: playerRoute,
982
- defaultAutoPlay: defaultAutoPlay,
1019
+ previewEnabled: previewEnabled,
1020
+ onPreviewEnabledChange: onPreviewEnabledChange,
983
1021
  limitFlow: limitFlow,
984
1022
  autoWakeUp: autoWakeUp,
985
1023
  extend: _objectSpread(_objectSpread({
package/lib/ui/ui.less CHANGED
@@ -160,6 +160,70 @@
160
160
  flex-direction: row;
161
161
  }
162
162
 
163
+ // 左下角横滑提示箭头:仅在内容溢出且未滑到对应边缘时由 JS 控制显示
164
+ .ipc-player-bottom-left-scroll-hint-arrow {
165
+ width: 7px;
166
+ height: 7px;
167
+ animation: ipc-player-bottom-left-scroll-hint-pulse 1.4s ease-in-out infinite;
168
+ }
169
+
170
+ .ipc-player-bottom-left-scroll-hint-arrow--right {
171
+ border-top: 2px solid rgba(255, 255, 255, 0.95);
172
+ border-right: 2px solid rgba(255, 255, 255, 0.95);
173
+ transform: rotate(45deg);
174
+ }
175
+
176
+ .ipc-player-bottom-left-scroll-hint-arrow--left {
177
+ border-top: 2px solid rgba(255, 255, 255, 0.95);
178
+ border-left: 2px solid rgba(255, 255, 255, 0.95);
179
+ transform: rotate(-45deg);
180
+ }
181
+
182
+ // button 模式:半透明黑圆底(点击热区)
183
+ .ipc-player-bottom-left-scroll-hint--button {
184
+ background: rgba(0, 0, 0, 0.45);
185
+ border-radius: 50%;
186
+ }
187
+
188
+ // button 模式:白色箭头
189
+ .ipc-player-bottom-left-scroll-hint-arrow--light {
190
+ border-top-color: rgba(255, 255, 255, 0.95);
191
+ border-right-color: rgba(255, 255, 255, 0.95);
192
+ border-left-color: rgba(255, 255, 255, 0.95);
193
+ }
194
+
195
+ // button 模式:常显、不做渐隐渐显动画
196
+ .ipc-player-bottom-left-scroll-hint-arrow--static {
197
+ animation: none;
198
+ opacity: 1;
199
+ }
200
+
201
+ // 右边缘淡出:把没完全露出的元素渐隐到透明。写成静态类,避免内联 mask 在原生层反复重绘闪烁
202
+ .ipc-player-bottom-left-scroll-fademask {
203
+ -webkit-mask-image: linear-gradient(
204
+ to right,
205
+ #000 calc(100% - 72px),
206
+ rgba(0, 0, 0, 0.25) calc(100% - 24px),
207
+ transparent 100%
208
+ );
209
+ mask-image: linear-gradient(
210
+ to right,
211
+ #000 calc(100% - 72px),
212
+ rgba(0, 0, 0, 0.25) calc(100% - 24px),
213
+ transparent 100%
214
+ );
215
+ }
216
+
217
+ @keyframes ipc-player-bottom-left-scroll-hint-pulse {
218
+ 0%,
219
+ 100% {
220
+ opacity: 0.55;
221
+ }
222
+ 50% {
223
+ opacity: 1;
224
+ }
225
+ }
226
+
163
227
  // 右下角子元素内容区域样式
164
228
  .ipc-player-bottom-right-content-container {
165
229
  display: flex;
@@ -233,20 +297,20 @@
233
297
 
234
298
  // 左下角全屏子元素容器
235
299
  .bottom-left-item-full-container {
236
- padding: 0 24px !important;
300
+ padding: 0 20px !important;
237
301
  flex-shrink: 0;
238
302
  }
239
303
 
240
304
  .bottom-left-item-full-container:first-of-type {
241
- padding: 0 24px 0 16px !important;
305
+ padding: 0 20px 0 12px !important;
242
306
  }
243
307
 
244
308
  .bottom-left-item-full-container:last-of-type {
245
- padding: 0 0 0 24px !important;
309
+ padding: 0 0 0 20px !important;
246
310
  }
247
311
 
248
312
  .bottom-left-item-full-container:only-of-type {
249
- padding: 0 0 0 16px !important;
313
+ padding: 0 0 0 12px !important;
250
314
  }
251
315
 
252
316
  // 右下角子元素容器
@@ -3,7 +3,7 @@ import { ComponentConfigProps } from '../../interface';
3
3
  import './tryExperience.less';
4
4
  type Props = ComponentConfigProps & {
5
5
  className?: string;
6
- hideTryExperienceMenu?: boolean;
6
+ hideSmartImageQualityState?: boolean;
7
7
  buttonState?: number;
8
8
  trialRemainingSec?: number;
9
9
  isPurchase?: boolean;
@@ -44,7 +44,7 @@ const pickIconByStatus = buttonState => {
44
44
  export const TryExperience = props => {
45
45
  const {
46
46
  className,
47
- hideTryExperienceMenu,
47
+ hideSmartImageQualityState,
48
48
  buttonState,
49
49
  trialRemainingSec,
50
50
  isPurchase,
@@ -145,7 +145,7 @@ export const TryExperience = props => {
145
145
  lockRef.current = false;
146
146
  }
147
147
  }, [buttonState, recording, isPurchase, trialRemainingSec]);
148
- if (hideTryExperienceMenu) {
148
+ if (hideSmartImageQualityState) {
149
149
  return null;
150
150
  }
151
151
  if (!imgSrc || !canOpenSettings || !gRawData) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ray-js/ipc-player-integration",
3
- "version": "0.0.35-beta.30",
3
+ "version": "0.0.35-beta.31",
4
4
  "description": "IPC 融合播放器",
5
5
  "main": "lib/index",
6
6
  "files": [
@@ -37,7 +37,7 @@
37
37
  "dependencies": {
38
38
  "@ray-js/direction-control": "^0.0.8",
39
39
  "@ray-js/ipc-ptz-zoom": "^0.0.3",
40
- "@ray-js/ray-ipc-player": "^2.1.1",
40
+ "@ray-js/ray-ipc-player": "^2.1.2",
41
41
  "@ray-js/ray-ipc-utils": "^1.1.15",
42
42
  "@ray-js/svg": "0.2.0",
43
43
  "clsx": "^1.2.1",