dsh-scroll-piano 0.3.3 → 0.3.5
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/README.md +4 -4
- package/lib/client.js +92 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
- 聊天区右边缘一条细彩色竖条(开启时淡淡发光),鼠标进入该区间即触发。
|
|
8
8
|
- 鼠标纵向移动 = 连续刮奏;竖条上有个白点跟随鼠标位置。
|
|
9
9
|
- 竖条上有**「我的消息」小地图刻度**:你每发一条消息,对应位置一根蓝色小横杠——**按「在整段对话里的位置」定位,与滚动无关**,所以不管你滚到对话多深,自己的消息始终标在那、点得到。
|
|
10
|
-
- **气泡只在鼠标贴近某条刻度(±16px)时出现**,内容就是那条消息的正文(最多 3
|
|
11
|
-
-
|
|
12
|
-
- 左下角一枚 `🎵 滚动钢琴 ON/OFF` 胶囊:点开关切换、右侧滑块调音量(设置存 `localStorage
|
|
10
|
+
- **气泡只在鼠标贴近某条刻度(±16px)时出现**,内容就是那条消息的正文(最多 3 行);**其余位置整个不显示**(不显示音名、不显示 MIDI 数字)。
|
|
11
|
+
- **点蓝色小横杠 → 跳到那条消息**(平滑滚动、滚到视口中央)。蓝条命中区放大到 18×12px(可视化仍是 7×2 细条,悬停时加宽变亮),比原来 2px 高好点得多;**点气泡也能跳**(两者同一个 `jumpTo`)。
|
|
12
|
+
- 左下角一枚 `🎵 滚动钢琴 ON/OFF` 胶囊:点开关切换、右侧滑块调音量(设置存 `localStorage`)。**胶囊可拖动**(`cursor:grab`):位移 >6px 判定为拖、松手位置存 `localStorage("dsh-scroll-piano:pillPos")`、拖完 400ms 内不吞点击;**按在音量滑块上不进拖动**,滑块照常调音量。
|
|
13
13
|
- 默认音域 C3–C6(3 个八度),只按半音触发(同音不重复触发)。
|
|
14
14
|
|
|
15
15
|
> 预览**只显示用户自己发的消息**,**不做「取全局最近一条」兜底**(否则同一条会在整片区域反复出现 = 重复显示),位置感交给刻度。**声音与预览彼此独立**——任何位置滑动都照常出声,只有预览挑消息。
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
| `14:assistant-stepN:M` | AI 消息 → 跳过 |
|
|
26
26
|
| `12:turn-processN` / `["process",…]` / `9:turn-tailN` | 过程 / 工具调用 / 轮次尾 → 跳过 |
|
|
27
27
|
|
|
28
|
-
刻度 y = **消息在滚动内容里的偏移 ÷ 内容总长 × 视口高**(`r.top − 视口top + scrollTop` 得到内容坐标,再归一化),**与 scrollTop 无关**——滚多远都不变,消息不会"滚出屏幕"而看不见。鼠标贴近某条刻度(±16px)就取它 `innerText` 的正文(去掉末尾 `HH:MM`
|
|
28
|
+
刻度 y = **消息在滚动内容里的偏移 ÷ 内容总长 × 视口高**(`r.top − 视口top + scrollTop` 得到内容坐标,再归一化),**与 scrollTop 无关**——滚多远都不变,消息不会"滚出屏幕"而看不见。鼠标贴近某条刻度(±16px)就取它 `innerText` 的正文(去掉末尾 `HH:MM` 时间戳)显示;**点那根蓝条**(容器 `click` 事件委托到 `.dshscp_tick`,读 `data-i` 找回消息元素)就 `scrollIntoView({ block: "center" })`。**只认这些 `data-*` 属性**——容器类名(`icaHSq_flowItem`、`ST7X_W_scrollBody`)都带 hash,不碰。
|
|
29
29
|
|
|
30
30
|
## 触发区:聊天区右边缘,不是窗口右边缘
|
|
31
31
|
|
package/lib/client.js
CHANGED
|
@@ -23,15 +23,17 @@ window.__ModuleLoader__.load({
|
|
|
23
23
|
|
|
24
24
|
//#region styles
|
|
25
25
|
const CSS = [
|
|
26
|
-
".dshscp_pill{box-sizing:border-box;position:fixed;left:132px;bottom:16px;z-index:2147483000;height:32px;display:inline-flex;align-items:center;gap:10px;padding:0 10px;border-radius:16px;background:var(--dsw-alias-bg-elevated,#1f2430);color:var(--dsw-alias-label-primary,#e6e8ee);box-shadow:0 4px 18px rgba(0,0,0,.35);font-size:12px;line-height:16px}",
|
|
26
|
+
".dshscp_pill{box-sizing:border-box;position:fixed;left:132px;bottom:16px;z-index:2147483000;height:32px;display:inline-flex;align-items:center;gap:10px;padding:0 10px;border-radius:16px;background:var(--dsw-alias-bg-elevated,#1f2430);color:var(--dsw-alias-label-primary,#e6e8ee);box-shadow:0 4px 18px rgba(0,0,0,.35);font-size:12px;line-height:16px;cursor:grab;touch-action:none}",
|
|
27
27
|
".dshscp_pill button{box-sizing:border-box;border:none;background:transparent;color:inherit;font:inherit;cursor:pointer;padding:0 2px;display:inline-flex;align-items:center;gap:6px}",
|
|
28
28
|
".dshscp_dot{flex:none;width:8px;height:8px;border-radius:50%;background:var(--dsw-alias-label-tertiary,#6b7280)}",
|
|
29
29
|
".dshscp_dotOn{background:#35c46a;box-shadow:0 0 6px rgba(53,196,106,.9)}",
|
|
30
30
|
".dshscp_vol{width:76px;accent-color:var(--dsw-alias-state-business-primary,#5b8def)}",
|
|
31
31
|
".dshscp_strip{position:fixed;top:0;right:0;width:3px;height:100%;z-index:2147482000;pointer-events:none;background:linear-gradient(180deg,rgba(91,141,239,.85),rgba(160,120,240,.85));opacity:0;transition:opacity .15s ease}",
|
|
32
32
|
".dshscp_stripOn{opacity:.55}",
|
|
33
|
-
".dshscp_ticks{position:fixed;top:0;right:0;width:
|
|
34
|
-
".dshscp_tick{position:absolute;right:0;width:
|
|
33
|
+
".dshscp_ticks{position:fixed;top:0;right:0;width:20px;height:100%;z-index:2147482000;pointer-events:none}",
|
|
34
|
+
".dshscp_tick{position:absolute;right:0;width:18px;height:12px;margin-top:-6px;pointer-events:auto;cursor:pointer}",
|
|
35
|
+
".dshscp_tick::after{content:\"\";position:absolute;right:0;top:50%;transform:translateY(-50%);width:7px;height:2px;border-radius:1px;background:rgba(91,141,239,.95);box-shadow:0 0 4px rgba(91,141,239,.7);transition:all .12s ease}",
|
|
36
|
+
".dshscp_tick:hover::after{width:16px;box-shadow:0 0 7px rgba(91,141,239,1)}",
|
|
35
37
|
".dshscp_marker{position:fixed;right:0;width:7px;height:7px;border-radius:50%;z-index:2147482001;pointer-events:none;background:#fff;box-shadow:0 0 8px rgba(160,120,240,.95);opacity:0;transition:opacity .1s ease}",
|
|
36
38
|
".dshscp_markerOn{opacity:1}",
|
|
37
39
|
".dshscp_prev{position:fixed;right:18px;z-index:2147482002;pointer-events:none;max-width:min(320px,42vw);padding:7px 10px;border-radius:9px;background:rgba(20,24,32,.95);color:#e6e8ee;box-shadow:0 6px 22px rgba(0,0,0,.45);opacity:0;transition:opacity .1s ease}",
|
|
@@ -39,7 +41,6 @@ window.__ModuleLoader__.load({
|
|
|
39
41
|
".dshscp_prevTop{display:flex;align-items:center;gap:8px;margin-bottom:4px}",
|
|
40
42
|
".dshscp_prevRole{flex:none;padding:1px 5px;border-radius:4px;background:rgba(91,141,239,.3);color:#bcd4ff;font-size:10px;line-height:14px}",
|
|
41
43
|
".dshscp_prevRoleAI{background:rgba(160,120,240,.28);color:#d6c4ff}",
|
|
42
|
-
".dshscp_prevHint{margin-left:auto;flex:none;color:#9aa2b1;font-size:10px;line-height:14px}",
|
|
43
44
|
".dshscp_prevText{font-size:12px;line-height:17px;color:#e6e8ee;overflow:hidden;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;word-break:break-word}"
|
|
44
45
|
].join("");
|
|
45
46
|
const tagId = "dsh-scroll-piano/scroll-piano.css";
|
|
@@ -240,6 +241,48 @@ window.__ModuleLoader__.load({
|
|
|
240
241
|
//#region component
|
|
241
242
|
function ScrollPiano() {
|
|
242
243
|
const [settings, setSettings] = useState(loadSettings);
|
|
244
|
+
// —— 左下角那条黑胶囊**可拖动**(音量滑块除外,它自己要收拖动事件)——
|
|
245
|
+
const [pillPos, setPillPos] = useState(() => {
|
|
246
|
+
try { return JSON.parse(localStorage.getItem("dsh-scroll-piano:pillPos") || "null"); } catch { return null; }
|
|
247
|
+
});
|
|
248
|
+
const pillPosRef = useRef(pillPos);
|
|
249
|
+
pillPosRef.current = pillPos;
|
|
250
|
+
const pillDragRef = useRef(null);
|
|
251
|
+
const pillDraggedRef = useRef(false);
|
|
252
|
+
const pillClearRef = useRef(0);
|
|
253
|
+
const onPillDown = (e) => {
|
|
254
|
+
// 按在音量滑块上不进拖动,否则调音量会被拖走
|
|
255
|
+
if (e.target && e.target.tagName === "INPUT") return;
|
|
256
|
+
if (e.pointerType === "mouse" && e.button !== 0) return;
|
|
257
|
+
pillDraggedRef.current = false;
|
|
258
|
+
const r = e.currentTarget.getBoundingClientRect();
|
|
259
|
+
pillDragRef.current = { x: e.clientX, y: e.clientY, left: r.left, top: r.top, moved: false };
|
|
260
|
+
try { e.currentTarget.setPointerCapture(e.pointerId); } catch {}
|
|
261
|
+
};
|
|
262
|
+
const onPillMove = (e) => {
|
|
263
|
+
const d = pillDragRef.current;
|
|
264
|
+
if (!d) return;
|
|
265
|
+
const dx = e.clientX - d.x;
|
|
266
|
+
const dy = e.clientY - d.y;
|
|
267
|
+
if (!d.moved && Math.abs(dx) + Math.abs(dy) > 6) d.moved = true;
|
|
268
|
+
if (!d.moved) return;
|
|
269
|
+
const left = Math.max(4, Math.min(window.innerWidth - 60, d.left + dx));
|
|
270
|
+
const top = Math.max(4, Math.min(window.innerHeight - 40, d.top + dy));
|
|
271
|
+
pillPosRef.current = { left, top };
|
|
272
|
+
setPillPos({ left, top });
|
|
273
|
+
};
|
|
274
|
+
const onPillUp = (e) => {
|
|
275
|
+
const d = pillDragRef.current;
|
|
276
|
+
pillDragRef.current = null;
|
|
277
|
+
try { e.currentTarget.releasePointerCapture(e.pointerId); } catch {}
|
|
278
|
+
if (!d || !d.moved) return;
|
|
279
|
+
pillDraggedRef.current = true;
|
|
280
|
+
const pos = pillPosRef.current;
|
|
281
|
+
if (pos) { try { localStorage.setItem("dsh-scroll-piano:pillPos", JSON.stringify(pos)); } catch {} }
|
|
282
|
+
// 拖完不一定紧跟 click → 400ms 自动清,别吞掉下一次真正点击
|
|
283
|
+
if (pillClearRef.current) window.clearTimeout(pillClearRef.current);
|
|
284
|
+
pillClearRef.current = window.setTimeout(() => { pillDraggedRef.current = false; }, 400);
|
|
285
|
+
};
|
|
243
286
|
const settingsRef = useRef(settings);
|
|
244
287
|
settingsRef.current = settings;
|
|
245
288
|
const lastMidiRef = useRef(null);
|
|
@@ -251,6 +294,7 @@ window.__ModuleLoader__.load({
|
|
|
251
294
|
const textRef = useRef(null);
|
|
252
295
|
const ticksRef = useRef(null);
|
|
253
296
|
const ticksDataRef = useRef([]); // [{y, el}] — 小地图刻度数据(showPreview 靠它找消息)
|
|
297
|
+
const jumpToRef = useRef(() => {});
|
|
254
298
|
const activeMsgRef = useRef(null);
|
|
255
299
|
|
|
256
300
|
const setOn = useCallback((on) => {
|
|
@@ -275,6 +319,26 @@ window.__ModuleLoader__.load({
|
|
|
275
319
|
let mo = null;
|
|
276
320
|
let moTimer = 0;
|
|
277
321
|
let tickTimer = 0;
|
|
322
|
+
/** 平滑滚动到某条消息(点蓝条 / 点气泡都走这里)。 */
|
|
323
|
+
const jumpTo = (el) => {
|
|
324
|
+
if (!el) return;
|
|
325
|
+
try { el.scrollIntoView({ behavior: "smooth", block: "center" }); }
|
|
326
|
+
catch { try { el.scrollIntoView(); } catch {} }
|
|
327
|
+
activeMsgRef.current = null;
|
|
328
|
+
if (prevRef.current) {
|
|
329
|
+
prevRef.current.classList.remove("dshscp_prevOn");
|
|
330
|
+
prevRef.current.style.pointerEvents = "none";
|
|
331
|
+
}
|
|
332
|
+
if (markerRef.current) markerRef.current.classList.remove("dshscp_markerOn");
|
|
333
|
+
};
|
|
334
|
+
jumpToRef.current = jumpTo;
|
|
335
|
+
/** 点竖条上的蓝色小横杠 → 跳到那条消息(事件委托,刻度是动态增删的)。 */
|
|
336
|
+
const onTickClick = (e) => {
|
|
337
|
+
const t = e.target;
|
|
338
|
+
if (!t || !t.classList || !t.classList.contains("dshscp_tick")) return;
|
|
339
|
+
const idx = parseInt(t.getAttribute("data-i"), 10);
|
|
340
|
+
if (idx >= 0) jumpTo(ticksDataRef.current[idx] && ticksDataRef.current[idx].el);
|
|
341
|
+
};
|
|
278
342
|
/** 竖条上给「我的消息」打小地图刻度:不管滚到哪,自己的消息始终标在那。 */
|
|
279
343
|
const refreshTicks = () => {
|
|
280
344
|
const box = ticksRef.current;
|
|
@@ -288,7 +352,10 @@ window.__ModuleLoader__.load({
|
|
|
288
352
|
i.className = "dshscp_tick";
|
|
289
353
|
box.appendChild(i);
|
|
290
354
|
}
|
|
291
|
-
for (let i = 0; i < data.length; i++)
|
|
355
|
+
for (let i = 0; i < data.length; i++) {
|
|
356
|
+
box.children[i].style.top = data[i].y + "px";
|
|
357
|
+
box.children[i].setAttribute("data-i", String(i));
|
|
358
|
+
}
|
|
292
359
|
};
|
|
293
360
|
// 对话增长 / 面板开合会改内容总长 → 节流到 150ms 重算(刻度与 scrollTop 无关,滚动不用重算)
|
|
294
361
|
const scheduleTicks = () => {
|
|
@@ -408,12 +475,14 @@ window.__ModuleLoader__.load({
|
|
|
408
475
|
document.addEventListener("pointermove", onMove, { passive: true });
|
|
409
476
|
document.addEventListener("pointerleave", onLeave);
|
|
410
477
|
// 不需要监听 scroll:刻度是按「内容偏移/内容总长」算的,与 scrollTop 无关
|
|
478
|
+
if (ticksRef.current) ticksRef.current.addEventListener("click", onTickClick);
|
|
411
479
|
window.addEventListener("blur", onLeave);
|
|
412
480
|
window.addEventListener("resize", placeAtChatEdge);
|
|
413
481
|
placeAtChatEdge();
|
|
414
482
|
return () => {
|
|
415
483
|
document.removeEventListener("pointermove", onMove);
|
|
416
484
|
document.removeEventListener("pointerleave", onLeave);
|
|
485
|
+
if (ticksRef.current) ticksRef.current.removeEventListener("click", onTickClick);
|
|
417
486
|
window.removeEventListener("blur", onLeave);
|
|
418
487
|
window.removeEventListener("resize", placeAtChatEdge);
|
|
419
488
|
if (ro) ro.disconnect();
|
|
@@ -423,7 +492,10 @@ window.__ModuleLoader__.load({
|
|
|
423
492
|
};
|
|
424
493
|
}, []);
|
|
425
494
|
|
|
426
|
-
|
|
495
|
+
/** 点气泡也跳(文字提示已去掉,主入口是点蓝条)。 */
|
|
496
|
+
const onBubbleClick = () => jumpToRef.current(activeMsgRef.current && activeMsgRef.current.el);
|
|
497
|
+
|
|
498
|
+
// 竖条 + 「我的消息」刻度(**点蓝色小横杠跳过去**)+ 位置圆点 + 预览气泡
|
|
427
499
|
return h(React.Fragment, null,
|
|
428
500
|
h("div", { ref: stripRef, className: "dshscp_strip" + (settings.on ? " dshscp_stripOn" : "") }),
|
|
429
501
|
h("div", { ref: ticksRef, className: "dshscp_ticks" }),
|
|
@@ -431,28 +503,26 @@ window.__ModuleLoader__.load({
|
|
|
431
503
|
h("div", {
|
|
432
504
|
ref: prevRef,
|
|
433
505
|
className: "dshscp_prev",
|
|
434
|
-
onClick:
|
|
435
|
-
const m = activeMsgRef.current;
|
|
436
|
-
if (!m || !m.el) return;
|
|
437
|
-
try { m.el.scrollIntoView({ behavior: "smooth", block: "center" }); }
|
|
438
|
-
catch { try { m.el.scrollIntoView(); } catch {} }
|
|
439
|
-
activeMsgRef.current = null;
|
|
440
|
-
if (prevRef.current) {
|
|
441
|
-
prevRef.current.classList.remove("dshscp_prevOn");
|
|
442
|
-
prevRef.current.style.pointerEvents = "none";
|
|
443
|
-
}
|
|
444
|
-
if (markerRef.current) markerRef.current.classList.remove("dshscp_markerOn");
|
|
445
|
-
}
|
|
506
|
+
onClick: onBubbleClick
|
|
446
507
|
},
|
|
447
508
|
h("div", { className: "dshscp_prevTop" },
|
|
448
|
-
h("span", { ref: roleRef, className: "dshscp_prevRole" }, "你")
|
|
449
|
-
h("span", { className: "dshscp_prevHint" }, "点击跳转")
|
|
509
|
+
h("span", { ref: roleRef, className: "dshscp_prevRole" }, "你")
|
|
450
510
|
),
|
|
451
511
|
h("div", { ref: textRef, className: "dshscp_prevText" })
|
|
452
512
|
),
|
|
453
|
-
h("div", {
|
|
513
|
+
h("div", {
|
|
514
|
+
className: "dshscp_pill",
|
|
515
|
+
onPointerDown: onPillDown,
|
|
516
|
+
onPointerMove: onPillMove,
|
|
517
|
+
onPointerUp: onPillUp,
|
|
518
|
+
onPointerCancel: onPillUp,
|
|
519
|
+
style: pillPos ? { left: pillPos.left + "px", top: pillPos.top + "px", bottom: "auto" } : undefined
|
|
520
|
+
},
|
|
454
521
|
h("button", {
|
|
455
|
-
onClick: () =>
|
|
522
|
+
onClick: () => {
|
|
523
|
+
if (pillDraggedRef.current) { pillDraggedRef.current = false; return; } // 刚才是拖,不是点
|
|
524
|
+
setOn(!settings.on);
|
|
525
|
+
},
|
|
456
526
|
title: settings.on ? "关闭滚动钢琴" : "开启滚动钢琴"
|
|
457
527
|
},
|
|
458
528
|
h("span", { className: "dshscp_dot" + (settings.on ? " dshscp_dotOn" : "") }),
|