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

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.
@@ -12,7 +12,7 @@ import { PlayState } from '../../interface';
12
12
  import { pauseTimeToHideAllComponent, playerTap } from '../../ui/constant';
13
13
  import { useLensZoom } from './useLensZoom';
14
14
  import ZoomTurntable from './turntable';
15
- import { CROP_RATIO_BOTTOM, CROP_RATIO_RIGHT, RELAYOUT_DELAYS, ZOOM_CANVAS_OVERSHOOT_BOTTOM, STYLE_OPTIONS_BOTTOM, STYLE_OPTIONS_RIGHT, VIBRATE_THROTTLE, zoomTurntableClose } from './constants';
15
+ import { CROP_RATIO_BOTTOM, CROP_RATIO_RIGHT, RELAYOUT_DELAYS, VIBRATE_FAST_STEPS, getStyleOptionsBottom, getVerticalPlayerHeight, getZoomLayoutDebugPx, getZoomVisibleHeightBottom, STYLE_OPTIONS_RIGHT, VIBRATE_THROTTLE, zoomTurntableClose } from './constants';
16
16
  import './zoomTurntableDock.less';
17
17
  /**
18
18
  * 当前页面 id,ty.nativeDisabled 需要它定位要禁用手势分发的页面。
@@ -47,11 +47,20 @@ export const ZoomTurntableDock = props => {
47
47
  } = props;
48
48
  const {
49
49
  screenType,
50
- playState
50
+ playState,
51
+ isVerticalFullLayout
51
52
  } = useStore({
52
53
  screenType: props.screenType,
53
- playState: props.playState
54
+ playState: props.playState,
55
+ isVerticalFullLayout: props.isVerticalFullLayout
54
56
  });
57
+
58
+ /**
59
+ * 竖屏全屏:底部工具条常驻,转盘停在它之上(canvas 底部不留触摸余量,否则会盖住按钮)。
60
+ * 普通竖屏:工具条已被 Entry 藏掉,转盘贴播放器底边(余量上下对称,底部那份沉到播放器外)。
61
+ * 判断必须与 Entry 的 absolutePosition 用同一套,否则 canvas 高度与 dock 位置对不上。
62
+ */
63
+ const isVerticalFull = screenType === 'vertical' && isVerticalFullLayout;
55
64
  const {
56
65
  multiLensReady,
57
66
  curM,
@@ -61,7 +70,8 @@ export const ZoomTurntableDock = props => {
61
70
  marks,
62
71
  ticks,
63
72
  beginDrag,
64
- applyZoom
73
+ applyZoom,
74
+ endDrag
65
75
  } = useLensZoom(_objectSpread(_objectSpread({}, props), {}, {
66
76
  skipLoad: true
67
77
  }));
@@ -76,13 +86,14 @@ export const ZoomTurntableDock = props => {
76
86
  const lastVibrateValueRef = useRef(curM);
77
87
 
78
88
  /**
79
- * 转盘起始值:只在打开转盘 / 重建画布时取一次,之后不再跟随 curM 回写。
80
- *
81
- * curM 会因为切镜头收尾、画面回读而变,受控回写会在拖动中把指针拽走 ——
82
- * 转盘自己维护值、只在落位时把结果下发出去,手感才连续。
89
+ * 转盘起始值:只在打开转盘 / 重建画布时取一次,作为首帧的 defaultValue。
83
90
  *
84
91
  * 取值时机要等镜头能力就绪:能力没就绪时 curM 只是 D_MIN 占位,
85
- * 拿它当起点会让转盘停在错误刻度(非受控之后没有回写能纠正了)。
92
+ * 拿它当起点会让转盘停在错误刻度。
93
+ *
94
+ * 稳态读数由下面的受控 `value={curM}` 负责跟随(手势缩放画面时读数要跟着动)。
95
+ * 早先只传 defaultValue、完全不回写,是担心「回写在拖动中把指针拽走」——
96
+ * 而 rjs 的 setValue 本身就有拖动中 + 松手后 300ms 冷却期的双重忽略,这个顾虑不成立。
86
97
  */
87
98
  const initialValueRef = useRef(null);
88
99
  const lastRemountRef = useRef(remountKey);
@@ -130,38 +141,58 @@ export const ZoomTurntableDock = props => {
130
141
  relayoutTimersRef.current = [];
131
142
  // 卸载兜底:手指还按着就被收起时,必须恢复原生手势
132
143
  setNativeDisabled(false);
144
+ // 拖动中被收起的话 onChangeEnd 不会来,draggingRef 会挂着 true 把后续的
145
+ // zoomLevelChange 全丢掉(手势缩放读数就不动了),这里补一次复位
146
+ endDrag();
133
147
  };
134
148
  }, []);
135
149
 
136
- // 诊断:把浮层与 canvas 的真实布局打出来,核对可拖区域到底覆盖到哪。
137
- // 期望(竖屏 HIT_PADDING_Y=56):dock 296、canvas288,canvas 顶距 dock 顶约 8px。
138
- // canvas 实际高度远小于 288,说明 rjs canvasWidth/Height 没随 styleOptions 更新,
139
- // 上半部分自然收不到 touch,会直接落到播放器上。
150
+ // 诊断:把浮层、canvas 与播放器容器的真实布局打出来,逐项核对两类历史坑:
151
+ // 1. dock 实测高 期望_dock absolutePosition rpx 换算对了
152
+ // (曾经给 194 只量到 101px,dock 只有 canvas 一半高,canvas 顶部被裁、收不到 touch)
153
+ // 2. canvas 实测高 ≈ 期望_canvas高 → rjs/index.js 的 computeCanvasViewport 读到了 ellipseRx/Ry
154
+ // (曾经它硬编码 258×175.5,DOM 208 高、绘制 186 高,画面被拉伸且顶部溢出被裁)
155
+ // canvas顶溢出 > 0 就是「手势不生效」的直接信号:溢出 dock 的那部分收不到 touch。
156
+ // canvas底距播底 应 ≈ 48(让出底部工具条):小于它就说明 canvas 压在按钮上、会吃掉按钮点击。
140
157
  useEffect(() => {
141
158
  const timer = setTimeout(() => {
142
159
  const query = ty.createSelectorQuery();
143
160
  query.select('.ipc-player-plugin-zoom-turntable-dock').boundingClientRect();
144
161
  query.select('.ipc-zoom-turntable-canvas').boundingClientRect();
145
- query.selectViewport().boundingClientRect();
162
+ query.select('.ipc-player-content').boundingClientRect();
146
163
  query.exec(res => {
147
164
  const dock = res === null || res === void 0 ? void 0 : res[0];
148
165
  const canvas = res === null || res === void 0 ? void 0 : res[1];
149
- const viewport = res === null || res === void 0 ? void 0 : res[2];
166
+ const player = res === null || res === void 0 ? void 0 : res[2];
167
+ const visibleH = getZoomVisibleHeightBottom();
168
+ const expect = getZoomLayoutDebugPx(isVerticalFull);
150
169
  console.log('[zoomTurntable] 浮层布局诊断', JSON.stringify({
151
170
  dock: dock && {
152
171
  top: dock.top,
153
- left: dock.left,
154
172
  w: dock.width,
155
173
  h: dock.height
156
174
  },
157
175
  canvas: canvas && {
158
176
  top: canvas.top,
159
- left: canvas.left,
160
177
  w: canvas.width,
161
178
  h: canvas.height
162
179
  },
163
- canvas顶距dock顶: dock && canvas ? canvas.top - dock.top : null,
164
- viewportH: viewport === null || viewport === void 0 ? void 0 : viewport.height
180
+ player: player && {
181
+ w: player.width,
182
+ h: player.height
183
+ },
184
+ // 16:9 推导值 vs 实测值,两者差得多说明容器不是满宽 16:9
185
+ 播放器高_推导: Math.round(getVerticalPlayerHeight()),
186
+ 竖屏全屏: isVerticalFull,
187
+ 期望_dock高: expect.dock高,
188
+ 期望_canvas高: expect.canvas高,
189
+ 背景带高: visibleH,
190
+ 背景带占比: player !== null && player !== void 0 && player.height ? +(visibleH / player.height).toFixed(3) : null,
191
+ // 竖屏全屏期望 ≈ 底部工具条高度(canvas 底边 = 工具条顶边);
192
+ // 普通竖屏期望 ≈ -HIT_PADDING_Y(沉到播放器底边以下)
193
+ canvas底距播底: canvas && player ? +(player.bottom - canvas.bottom).toFixed(1) : null,
194
+ // > 0 说明 canvas 顶部露在 dock 外被裁,那部分收不到 touch
195
+ canvas顶溢出: dock && canvas ? +(dock.top - canvas.top).toFixed(1) : null
165
196
  }));
166
197
  });
167
198
  }, 300);
@@ -213,7 +244,8 @@ export const ZoomTurntableDock = props => {
213
244
  lastVibrateValueRef.current = value;
214
245
  beginDrag(value);
215
246
 
216
- // 跨过主刻度(各镜头光学倍数 / 上限)时给更明显的反馈,形成挡位感
247
+ // 跨过主刻度(各镜头光学倍数 / 上限)时给更明显的反馈,形成挡位感。
248
+ // 这一下不受节流约束 —— 挡位感是转盘手感的骨架,丢一次就没了
217
249
  const crossedMark = marks.some(mark => prev < mark.value && value >= mark.value || prev > mark.value && value <= mark.value);
218
250
  const now = Date.now();
219
251
  if (crossedMark) {
@@ -221,10 +253,17 @@ export const ZoomTurntableDock = props => {
221
253
  vibrate('medium');
222
254
  return;
223
255
  }
224
- if (now - lastVibrateAtRef.current >= VIBRATE_THROTTLE) {
225
- lastVibrateAtRef.current = now;
226
- vibrate('light');
227
- }
256
+
257
+ // 距上一次震动太近就跳过。也顺带给 medium 留出静默期:
258
+ // 刚过主刻度又紧跟一下 light,会把挡位那一下的辨识度糊掉
259
+ if (now - lastVibrateAtRef.current < VIBRATE_THROTTLE) return;
260
+
261
+ // 一次 onChange 跨了好几格 = 正在快速甩动,逐格震分辨不出来,只会连成一片。
262
+ // 这时不震普通刻度,把反馈让给主刻度的挡位感
263
+ const movedSteps = step > 0 ? Math.abs(value - prev) / step : 1;
264
+ if (movedSteps > VIBRATE_FAST_STEPS) return;
265
+ lastVibrateAtRef.current = now;
266
+ vibrate('light');
228
267
  });
229
268
  const handleChangeEnd = useMemoizedFn(value => {
230
269
  applyZoom(value);
@@ -241,6 +280,18 @@ export const ZoomTurntableDock = props => {
241
280
  setNativeDisabled(false);
242
281
  });
243
282
 
283
+ /**
284
+ * 点在转盘空白处(未命中刻度带)= 点画面 → 收起转盘。
285
+ *
286
+ * canvas 覆盖了播放器下部很大一片,且 touch 事件被它 preventDefault + stopPropagation,
287
+ * 播放器收不到这一下 tap,「点画面收起」在转盘范围内整个失效 ——
288
+ * 表现就是第一次点没反应、第二次点(落到画面上部)才收起。
289
+ */
290
+ const handleBlankTap = useMemoizedFn(() => {
291
+ setNativeDisabled(false);
292
+ requestClose();
293
+ });
294
+
244
295
  // 定位由承载的 CoverView 负责(absolutePosition),这里只做布局容器。
245
296
  const turntableStyle = useMemo(() => ({
246
297
  width: isRight ? 'auto' : '100%',
@@ -252,18 +303,19 @@ export const ZoomTurntableDock = props => {
252
303
  }), [isRight]);
253
304
 
254
305
  /**
255
- * canvas 的承载层。竖屏用负 margin canvas 底边探出播放器底边,
256
- * 浮层本身则贴底不下沉 —— 这样 canvas 顶部(刻度线那一带)才不会被 CoverView 裁掉,
257
- * 而绘制内容的视觉位置与探出效果都和原来一致。
306
+ * canvas 的承载层:只负责居中,不再用负 margin canvas 往下推 ——
307
+ * 实测负 margin CoverView 里不生效,探出去的那截会原地显示出来,
308
+ * 转盘整体被顶高一截、底边还留一条透明缝。
309
+ * 现在椭圆截面精确等于可见高度(CROP_RATIO_BOTTOM),整体下沉交给 dock 的
310
+ * dock 的 bottom(竖屏全屏为正、普通竖屏为负),canvas 尺寸与命中区保持原样,手势不受影响。
258
311
  */
259
- const canvasHolderStyle = useMemo(() => isRight ? {
312
+ const canvasHolderStyle = useMemo(() => ({
260
313
  display: 'flex',
261
314
  justifyContent: 'center'
262
- } : {
263
- display: 'flex',
264
- justifyContent: 'center',
265
- marginBottom: `-${ZOOM_CANVAS_OVERSHOOT_BOTTOM}px`
266
- }, [isRight]);
315
+ }), []);
316
+
317
+ /** 竖屏几何按容器 16:9 推导(转盘占播放器高的 1/3),横屏沿用设计稿比例 */
318
+ const styleOptions = useMemo(() => isRight ? STYLE_OPTIONS_RIGHT : getStyleOptionsBottom(isVerticalFull), [isRight, isVerticalFull]);
267
319
  if (!multiLensReady || playState !== PlayState.PLAYING) {
268
320
  return null;
269
321
  }
@@ -280,7 +332,12 @@ export const ZoomTurntableDock = props => {
280
332
  style: canvasHolderStyle
281
333
  }, /*#__PURE__*/React.createElement(ZoomTurntable, {
282
334
  key: `${isRight ? 'right' : 'bottom'}-${remountKey}`,
283
- defaultValue: initialValue,
335
+ defaultValue: initialValue
336
+ // 受控回写:手势缩放画面时读数要跟着走。
337
+ // rjs 的 setValue 自带保护 —— 拖动中、以及松手后 300ms 冷却期内一律忽略回写,
338
+ // 所以不会出现「拖动被回写拽走」(那正是当初只传 defaultValue 的顾虑)。
339
+ ,
340
+ value: curM,
284
341
  min: min,
285
342
  max: max,
286
343
  step: step,
@@ -291,10 +348,11 @@ export const ZoomTurntableDock = props => {
291
348
  orientation: isRight ? 'right' : 'bottom',
292
349
  cropRatio: isRight ? CROP_RATIO_RIGHT : CROP_RATIO_BOTTOM,
293
350
  activeColor: "#FFFFFF",
294
- styleOptions: isRight ? STYLE_OPTIONS_RIGHT : STYLE_OPTIONS_BOTTOM,
351
+ styleOptions: styleOptions,
295
352
  onChange: handleChange,
296
353
  onChangeEnd: handleChangeEnd,
297
354
  onTouchStart: handleTouchStart,
298
- onTouchEnd: handleTouchEnd
355
+ onTouchEnd: handleTouchEnd,
356
+ onBlankTap: handleBlankTap
299
357
  })));
300
358
  };
@@ -2,7 +2,7 @@ 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.map.js";
4
4
  import React, { useContext, useEffect, useRef, useState } from 'react';
5
- import { View, Text } from '@ray-js/ray';
5
+ import { View, Text, usePageEvent } from '@ray-js/ray';
6
6
  import clsx from 'clsx';
7
7
  import { useMemoizedFn } from 'ahooks';
8
8
  import { useStore } from '../../ctx/store';
@@ -13,9 +13,9 @@ import { Storage } from '../../utils/storage';
13
13
  import { useLensZoom } from './useLensZoom';
14
14
  import { ZoomTurntableDock } from './zoomTurntableDock';
15
15
  import { ZoomTurntableTip } from './zoomTurntableTip';
16
- import { measureEntryVisible } from './measureVisible';
16
+ import { measureBottomBarHeight, measureEntryVisible } from './measureVisible';
17
17
  import { ZOOM_ENTRY_ANCHOR_CLS } from './entryAnchor';
18
- import { LONG_PRESS_MS, MOVE_TOLERANCE, ZOOM_DOCK_BOTTOM, ZOOM_DOCK_HEIGHT, ZOOM_DOCK_RIGHT, ZOOM_DOCK_Z_INDEX, zoomTurntableClose, zoomTurntableDockId, zoomTurntableOpen, zoomTurntableTipId } from './constants';
18
+ import { FORCE_SHOW_ZOOM_TIP, LONG_PRESS_MS, MOVE_TOLERANCE, TIP_RESHOW_DELAY, getZoomDockBottom, getZoomDockHeight, getZoomDockRight, ZOOM_DOCK_Z_INDEX, zoomTurntableClose, zoomTurntableDockId, zoomTurntableOpen, zoomTurntableTipId } from './constants';
19
19
  import './zoomTurntableEntry.less';
20
20
  /**
21
21
  * 焦距转盘入口:左下角工具条中的图标,长按弹出转盘。
@@ -39,12 +39,22 @@ export const ZoomTurntableEntry = props => {
39
39
  const {
40
40
  playState,
41
41
  screenType,
42
- brandColor
42
+ isVerticalFullLayout
43
43
  } = useStore({
44
44
  playState: props.playState,
45
45
  screenType: props.screenType,
46
- brandColor: props.brandColor
46
+ isVerticalFullLayout: props.isVerticalFullLayout
47
47
  });
48
+
49
+ /**
50
+ * 竖屏两种布局,转盘的落位与是否让路完全不同:
51
+ * - 竖屏全屏(isVerticalFullLayout):底部工具条常驻在屏幕底部,转盘停在工具条**之上**,
52
+ * 工具条保持可见(能看到当前镜头、能直接点右下角那排按钮);
53
+ * - 普通竖屏:播放器只是页面顶部的一块,长按 tab 时把底部元素藏掉,
54
+ * 转盘**贴播放器底边**显示。
55
+ * 两种方式下转盘都落在播放器的底部区域。
56
+ */
57
+ const isVerticalFull = screenType === 'vertical' && isVerticalFullLayout;
48
58
  const {
49
59
  multiLensReady,
50
60
  lenses,
@@ -82,22 +92,39 @@ export const ZoomTurntableEntry = props => {
82
92
  dockVisibleRef.current = false;
83
93
  setDockVisible(false);
84
94
  deleteContent('absolute', zoomTurntableDockId);
95
+ // 竖屏没藏过,这一发是幂等的(横屏才真正需要它恢复右侧控件)
85
96
  event.emit(showAllComponent);
86
97
  event.emit(startTimeToHideAllComponent);
87
98
  });
88
- const openDock = useMemoizedFn(() => {
99
+ const openDock = useMemoizedFn(async () => {
89
100
  if (dockVisibleRef.current) {
90
101
  closeDock();
91
102
  return;
92
103
  }
104
+ /**
105
+ * 竖屏全屏要让出底部工具条,高度按**实测**取:ui.tsx 给它写的是 48px,
106
+ * 而 `-full` 变体在 less 里还叠了 `padding-bottom: env(safe-area-inset-bottom)`,
107
+ * 实际占位跟机型 safe-area 有关,硬编码必然对不上。
108
+ * 长按本身已经等了 500ms,这里再等一帧测量,体感无差别;测不到就退回 48。
109
+ */
110
+ const barHeight = isVerticalFull ? await measureBottomBarHeight() : null;
93
111
  event.emit(widgetClick, {
94
112
  widgetId: widgetLabs.ZOOM_TURNTABLE
95
113
  });
96
114
  // PTZ 与转盘都占底部 / 右侧,互斥
97
115
  event.emit('ptzControlHide');
98
- // 其余元素隐藏并暂停自动隐藏倒计时。
99
- // 不要 emit('disablePlayerTap', true):点播放器收起转盘依赖 ui.tsx 正常派发 playerTap。
100
- event.emit(hideAllComponent);
116
+ /**
117
+ * 只有竖屏全屏**不隐藏**其余元素:底部工具条常驻,转盘停在它上方 ——
118
+ * 这样能看到当前在哪个镜头,也能直接点右下角那排按钮,收起不必先点画面。
119
+ *
120
+ * 普通竖屏与横屏仍旧先藏:前者转盘要贴播放器底边、正压在工具条位置上,
121
+ * 后者转盘占右侧、与右侧控件正面重叠。
122
+ *
123
+ * 不要 emit('disablePlayerTap', true):点播放器收起转盘依赖 ui.tsx 正常派发 playerTap。
124
+ */
125
+ if (!isVerticalFull) {
126
+ event.emit(hideAllComponent);
127
+ }
101
128
  event.emit(pauseTimeToHideAllComponent);
102
129
  if (!hasContent('absolute', zoomTurntableDockId)) {
103
130
  // 必须用 CoverView 承载(不能设 absoluteNoCoverView):播放器是原生组件、层级更高,
@@ -107,14 +134,17 @@ export const ZoomTurntableEntry = props => {
107
134
  content: dockProps => /*#__PURE__*/React.createElement(ZoomTurntableDock, dockProps),
108
135
  absolutePosition: screenType === 'full' ? {
109
136
  top: 0,
110
- right: ZOOM_DOCK_RIGHT,
137
+ right: getZoomDockRight(),
111
138
  bottom: 0,
112
139
  zIndex: ZOOM_DOCK_Z_INDEX
113
140
  } : {
114
141
  left: 0,
115
142
  right: 0,
116
- bottom: ZOOM_DOCK_BOTTOM,
117
- height: ZOOM_DOCK_HEIGHT,
143
+ // 一律是带单位的 px 字符串 —— 裸数字会被当 rpx,见 constants 里 px() 的说明。
144
+ // 竖屏全屏让出底部工具条(实测高度,避免与 ui.tsx 的写死值不同步);
145
+ // 普通竖屏工具条已藏,直接下沉一份底部触摸余量贴住播放器底边。
146
+ bottom: getZoomDockBottom(isVerticalFull, barHeight),
147
+ height: getZoomDockHeight(isVerticalFull),
118
148
  zIndex: ZOOM_DOCK_Z_INDEX
119
149
  },
120
150
  initProps: _objectSpread(_objectSpread({}, props), {}, {
@@ -138,6 +168,17 @@ export const ZoomTurntableEntry = props => {
138
168
  };
139
169
  }, [event, closeDock]);
140
170
 
171
+ /**
172
+ * 横竖屏切换、竖屏布局切换(展开/收起竖屏全屏)时都先收起转盘。
173
+ *
174
+ * dock 的 absolutePosition 只在 addContent 那一刻定死,三套落位差别很大
175
+ * (横屏右侧 / 竖屏全屏的工具条之上 / 普通竖屏贴播放器底边),
176
+ * 不收起的话浮层会留在旧位置上 —— 横竖屏之间尤其明显,连朝向都不对。
177
+ */
178
+ useEffect(() => {
179
+ if (dockVisibleRef.current) closeDock();
180
+ }, [screenType, isVerticalFull, closeDock]);
181
+
141
182
  // 卸载 / 停流时兜底收起
142
183
  useEffect(() => {
143
184
  if (playState !== PlayState.PLAYING && dockVisibleRef.current) {
@@ -160,19 +201,58 @@ export const ZoomTurntableEntry = props => {
160
201
  }, [multiLensReady, lenses.length, event]);
161
202
 
162
203
  // ---------------- 圈选引导 ----------------
204
+
205
+ /**
206
+ * 把引导收起,并允许之后重新判定一次。
207
+ *
208
+ * 引导是挂在 absolute 槽位的浮层,停流 / 进入后台时它还留在最上层 ——
209
+ * 回到前台会先看见它盖在画面上,紧接着重新拉流把 playState 打断,它又消失,
210
+ * 观感就是「闪一下」。所以离开时主动收起,等重新出流稳定后再按可见性重新判定。
211
+ * 注意不写已读标记:用户还没点「知道了」,下次仍应提示。
212
+ */
213
+ const dismissTipForRestart = useMemoizedFn(() => {
214
+ if (!hasContent('absolute', zoomTurntableTipId)) return;
215
+ deleteContent('absolute', zoomTurntableTipId);
216
+ tipCheckedRef.current = false;
217
+ });
218
+
219
+ // 进入后台:先收起,避免回前台的第一帧就看到它压在重连画面上
220
+ usePageEvent('onHide', dismissTipForRestart);
221
+
222
+ // 停流 / 重连:同样先收起,等回到 PLAYING 再重新判定
223
+ useEffect(() => {
224
+ if (playState !== PlayState.PLAYING) {
225
+ dismissTipForRestart();
226
+ }
227
+ }, [playState, dismissTipForRestart]);
228
+
229
+ // 横竖屏切换:入口位置整个变了,旧引导的圈选坐标已失效,必须先收起再按新布局重测。
230
+ // 不收的话横屏、竖屏两套引导会同时留在屏幕上(absolute 槽位不会自动清理)。
231
+ useEffect(() => {
232
+ dismissTipForRestart();
233
+ }, [screenType, dismissTipForRestart]);
234
+
163
235
  // 入口在横向 ScrollView 内可能被裁掉,只有真正可见时才提示;能力异步就绪,故判定在 multiLensReady 之后
164
236
  useEffect(() => {
165
237
  if (disableTip || !multiLensReady || playState !== PlayState.PLAYING) return undefined;
166
238
  if (tipCheckedRef.current) return undefined;
167
239
  let cancelled = false;
168
240
  const storage = new Storage(devId);
169
- (async () => {
241
+ // 刚出流时布局还在沉降(左下角元素陆续渲染、宽度还在长),等一小会儿再测再展示,
242
+ // 免得引导刚画好就因为入口位置变化而歪掉,也避免出流瞬间弹出来
243
+ const timer = setTimeout(async () => {
170
244
  // key 不存在时 getStorage 会 fail,这里不能让它把整条链路带崩
171
245
  const read = await storage.get(zoomTurntableTipId).catch(() => null);
172
- if (cancelled || read) return;
246
+ if (cancelled) return;
247
+ // 调试开关打开时忽略已读记录,每次都展示
248
+ if (read && !FORCE_SHOW_ZOOM_TIP) return;
173
249
  const rect = await measureEntryVisible();
174
250
  if (cancelled || !rect) return;
175
251
  tipCheckedRef.current = true;
252
+ // ctx.addContent 不按 id 去重,重复添加会在屏幕上留下两份引导,这里先清一次
253
+ if (hasContent('absolute', zoomTurntableTipId)) {
254
+ deleteContent('absolute', zoomTurntableTipId);
255
+ }
176
256
  addContent('absolute', {
177
257
  id: zoomTurntableTipId,
178
258
  content: tipProps => /*#__PURE__*/React.createElement(ZoomTurntableTip, tipProps),
@@ -181,9 +261,10 @@ export const ZoomTurntableEntry = props => {
181
261
  anchorRect: rect
182
262
  })
183
263
  });
184
- })();
264
+ }, TIP_RESHOW_DELAY);
185
265
  return () => {
186
266
  cancelled = true;
267
+ clearTimeout(timer);
187
268
  };
188
269
  }, [disableTip, multiLensReady, playState, screenType, devId]);
189
270
 
@@ -254,9 +335,6 @@ export const ZoomTurntableEntry = props => {
254
335
  'ipc-player-plugin-zoom-turntable-tab-full': screenType === 'full',
255
336
  'ipc-player-plugin-zoom-turntable-tab-active': active
256
337
  }),
257
- style: active && dockVisible ? {
258
- backgroundColor: brandColor
259
- } : undefined,
260
338
  onTouchStart: onTouchStart,
261
339
  onTouchMove: onTouchMove,
262
340
  onTouchEnd: () => onTouchEnd(index),
@@ -7,7 +7,7 @@ import { widgetClick, widgetLabs } from '../../ui/constant';
7
7
  import { Storage } from '../../utils/storage';
8
8
  import Strings from '../../i18n';
9
9
  import { measurePlayerRect } from './measureVisible';
10
- import { TIP_RING_SIZE, zoomTurntableTipId } from './constants';
10
+ import { FORCE_SHOW_ZOOM_TIP, TIP_LINE_TOTAL, TIP_PANEL_EDGE_GAP, TIP_PANEL_LINE_GAP, TIP_PANEL_WIDTH, TIP_RING_PADDING, zoomTurntableTipId } from './constants';
11
11
  import './zoomTurntableTip.less';
12
12
  /**
13
13
  * 焦距入口圈选引导(一次性)。
@@ -26,51 +26,63 @@ export const ZoomTurntableTip = props => {
26
26
  } = useStore({
27
27
  playState: props.playState
28
28
  });
29
- const [ringStyle, setRingStyle] = useState(null);
29
+ const [layout, setLayout] = useState(null);
30
30
 
31
- // 把入口的视口坐标换算成播放器容器内坐标
31
+ // 把入口的视口坐标换算成播放器容器内坐标。
32
+ // 圈选框按入口实测 rect 自适应:入口是横向倍率胶囊,宽度随镜头数(2 个约 96px、
33
+ // 3 个约 132px)变化,用固定尺寸框不住。
32
34
  useEffect(() => {
33
35
  if (!anchorRect) return undefined;
34
36
  let cancelled = false;
35
37
  measurePlayerRect().then(player => {
36
38
  if (cancelled || !player) return;
37
- const centerX = anchorRect.left - player.left + anchorRect.width / 2;
38
- setRingStyle({
39
- left: `${centerX - TIP_RING_SIZE / 2}px`,
40
- bottom: `${player.bottom - anchorRect.bottom - (TIP_RING_SIZE - anchorRect.height) / 2}px`,
41
- width: `${TIP_RING_SIZE}px`,
42
- height: `${TIP_RING_SIZE}px`
39
+ const ringWidth = anchorRect.width + TIP_RING_PADDING * 2;
40
+ const ringHeight = anchorRect.height + TIP_RING_PADDING * 2;
41
+ const ringLeft = anchorRect.left - player.left - TIP_RING_PADDING;
42
+ const ringBottom = player.bottom - anchorRect.bottom - TIP_RING_PADDING;
43
+ // 文案面板以圈选中心对齐,但不越出播放器左右边缘
44
+ const maxLeft = Math.max(player.width - TIP_PANEL_WIDTH - TIP_PANEL_EDGE_GAP, TIP_PANEL_EDGE_GAP);
45
+ const panelLeft = Math.min(Math.max(ringLeft + ringWidth / 2 - TIP_PANEL_WIDTH / 2, TIP_PANEL_EDGE_GAP), maxLeft);
46
+ setLayout({
47
+ ring: {
48
+ left: `${ringLeft}px`,
49
+ bottom: `${ringBottom}px`,
50
+ width: `${ringWidth}px`,
51
+ height: `${ringHeight}px`
52
+ },
53
+ panel: {
54
+ left: `${panelLeft}px`,
55
+ bottom: `${ringBottom + ringHeight + TIP_LINE_TOTAL + TIP_PANEL_LINE_GAP}px`,
56
+ width: `${TIP_PANEL_WIDTH}px`
57
+ }
43
58
  });
44
59
  });
45
60
  return () => {
46
61
  cancelled = true;
47
62
  };
48
63
  }, [anchorRect]);
49
- if (playState !== PlayState.PLAYING || !ringStyle) {
64
+ if (playState !== PlayState.PLAYING || !layout) {
50
65
  return null;
51
66
  }
52
67
  return /*#__PURE__*/React.createElement(CoverView, null, /*#__PURE__*/React.createElement(View, {
53
68
  className: clsx('ipc-player-plugin-zoom-turntable-tip-ring'),
54
- style: ringStyle
55
- }), /*#__PURE__*/React.createElement(Text, {
56
- className: clsx('ipc-player-plugin-zoom-turntable-tip-text'),
57
- style: {
58
- left: ringStyle.left,
59
- bottom: `calc(${ringStyle.bottom} + 116px)`
60
- }
69
+ style: layout.ring
70
+ }), /*#__PURE__*/React.createElement(View, {
71
+ className: clsx('ipc-player-plugin-zoom-turntable-tip-panel'),
72
+ style: layout.panel
73
+ }, /*#__PURE__*/React.createElement(Text, {
74
+ className: clsx('ipc-player-plugin-zoom-turntable-tip-text')
61
75
  }, Strings.getLang('ipc_player_zoom_turntable_tip_text')), /*#__PURE__*/React.createElement(View, {
62
76
  className: clsx('ipc-player-plugin-zoom-turntable-tip-button'),
63
- style: {
64
- left: ringStyle.left,
65
- bottom: `calc(${ringStyle.bottom} + 56px)`
66
- },
67
77
  onClick: () => {
68
78
  props.event.emit(widgetClick, {
69
79
  widgetId: widgetLabs.ZOOM_TURNTABLE_TIP
70
80
  });
71
81
  deleteContent('absolute', zoomTurntableTipId);
82
+ // 调试开关打开时不落已读标记,免得自测一次就再也看不到
83
+ if (FORCE_SHOW_ZOOM_TIP) return;
72
84
  const storage = new Storage(devId);
73
85
  storage.set(zoomTurntableTipId, true);
74
86
  }
75
- }, Strings.getLang('ipc_player_tip_confirm')));
87
+ }, Strings.getLang('ipc_player_tip_confirm'))));
76
88
  };
@@ -10,10 +10,13 @@
10
10
  font-size: 14px;
11
11
  color: #fff;
12
12
 
13
+ // 圈选框:尺寸与位置按入口实测 rect 由 JS 内联注入。
14
+ // 入口是横向倍率胶囊,用圆角矩形贴合,不用正圆(正圆框不住宽度、上下还会超出)。
13
15
  .ipc-player-plugin-zoom-turntable-tip-ring {
14
16
  position: absolute;
17
+ box-sizing: border-box;
15
18
  border: 1px dashed rgba(255, 255, 255, 0.5);
16
- border-radius: 50%;
19
+ border-radius: 12px;
17
20
  // 超大扩散阴影把全屏压暗,只露出被圈选的入口
18
21
  box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.7);
19
22
 
@@ -23,13 +26,13 @@
23
26
  position: absolute;
24
27
  left: 50%;
25
28
  width: 1px;
26
- height: 40px;
29
+ height: 17px;
27
30
  box-sizing: border-box;
28
31
  border-left: 1px dashed rgba(255, 255, 255, 0.5);
29
32
  transform: translateX(-50%) translateY(-100%);
30
33
  }
31
34
 
32
- // 引线末端的小圆点
35
+ // 引线末端的小圆点:17 虚线 + 4 圆点
33
36
  &::before {
34
37
  content: '';
35
38
  position: absolute;
@@ -38,18 +41,31 @@
38
41
  height: 4px;
39
42
  border-radius: 50%;
40
43
  background-color: rgba(255, 255, 255, 0.5);
41
- transform: translateX(-50%) translateY(-44px);
44
+ transform: translateX(-50%) translateY(-21px);
42
45
  }
43
46
  }
44
47
 
45
- .ipc-player-plugin-zoom-turntable-tip-text {
48
+ // 文案 + 按钮面板:整体定位一次并对齐圈选框中心,内部居中堆叠,
49
+ // 间距交给布局,不再靠魔法数各自偏移
50
+ .ipc-player-plugin-zoom-turntable-tip-panel {
46
51
  position: absolute;
52
+ display: flex;
53
+ flex-direction: column;
54
+ align-items: center;
55
+ }
56
+
57
+ .ipc-player-plugin-zoom-turntable-tip-text {
47
58
  max-width: 200px;
59
+ line-height: 1.4;
60
+ text-align: center;
48
61
  }
49
62
 
50
63
  .ipc-player-plugin-zoom-turntable-tip-button {
51
- position: absolute;
52
- padding: 12px 32px;
64
+ margin-top: 12px;
65
+ // multiCamera 的 tile-tip(12/32)收紧一圈:引导只是一句短提示,按钮不必那么重
66
+ padding: 7px 20px;
67
+ font-size: 12px;
68
+ line-height: 1.4;
53
69
  border: 1px solid rgba(255, 255, 255, 0.5);
54
70
  border-radius: 250px;
55
71
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ray-js/ipc-player-integration",
3
- "version": "0.0.50-beta.2",
3
+ "version": "0.0.50-beta.4",
4
4
  "description": "IPC 融合播放器",
5
5
  "main": "lib/index",
6
6
  "files": [