Skip to content

Commit ac1fb35

Browse files
committed
新增只针对 compact 播放器内音量弹层的 mutation target
.music-bar-volume-container.expanded 变化会触发 neko:compact-interaction-geometry-refresh 音量按钮打开/关闭、点击外部收起,两条播放器路径都会主动刷新 geometry
1 parent 25c124e commit ac1fb35

3 files changed

Lines changed: 35 additions & 4 deletions

File tree

docs/design/compact-chat-mode-design.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@
126126
- 监听 `neko:compact-surface-drag-grab`(来自 React 工具轮盘原点拖拽),非 Electron 时以事件坐标为锚启动 compact surface 本体拖拽(复用既有 startDrag/全局 mousemove/mouseup 与落点 click 守卫)。Electron 由 `preload-chat-react.js` 监听同一事件改走原生窗口拖拽。
127127
5. `static/app-buttons.js` 是发送桥之一。compact history 文本发送必须带清晰 session / request 语义,不能让已有 composer 附件在 deferred send 中被误带上。
128128
6. 语音模式 / `composerHidden` 下的 history drop 只保留前端拖拽、命中和收束动效;真实发送必须在 `sendCompactHistoryDropPayload` 边界跳过,不能通过改 React 拖拽 phase 或样式来伪装。
129-
7. `static/music_ui.js` 的音乐播放器在 compact 模式下优先挂到常驻 `.compact-music-player-mount#music-player-mount`;历史关闭或卸载不能把播放器挪回 composer fallback,但播放器视觉显隐必须跟随历史打开、closing、closed 状态,也不能被通用 `#music-player-mount` 样式撑成超过 compact surface 的横向尺寸。
129+
7. `static/music_ui.js` 的音乐播放器在 compact 模式下优先挂到常驻 `.compact-music-player-mount#music-player-mount`;历史关闭或卸载不能把播放器挪回 composer fallback,但播放器视觉显隐必须跟随历史打开、closing、closed 状态,也不能被通用 `#music-player-mount` 样式撑成超过 compact surface 的横向尺寸;音量弹层展开/收起时必须刷新 compact geometry,避免浮出播放器原生矩形的滑块看得见但不可点
130130

131131
### NEKO-PC 桌面壳
132132

@@ -373,7 +373,7 @@ Compact 历史默认在初次启动时显示。历史列表本身由常驻展开
373373
13. 操作栏隐藏时退出选择模式:必须清空当前选中项,并禁止继续通过点击或键盘选择;拖拽源识别和拖拽发送不受这个选择模式限制。
374374
14. 操作栏包含选择和导出动作,如计数、全选、取消/清空、反选、导出预览等;操作栏自身进入 history hit region。
375375
15. 选择状态、导出预览和操作栏显示状态由 React state 管理;操作栏状态可以跨历史显隐保留,但只在历史实际打开时算作可见。
376-
16. 音乐播放器有独立 `.compact-music-player-mount#music-player-mount`,它与历史消息面板分离并作为 `musicPlayer` 几何项进入 compact surface;历史关闭/卸载后播放器必须继续停留在该独立挂载点,但视觉上要随历史一起收起和展开;横向尺寸必须限制在 compact surface 宽度内;历史记录底部必须为播放器高度和阴影预留间距,不能与播放器重叠。
376+
16. 音乐播放器有独立 `.compact-music-player-mount#music-player-mount`,它与历史消息面板分离并作为 `musicPlayer` 几何项进入 compact surface;历史关闭/卸载后播放器必须继续停留在该独立挂载点,但视觉上要随历史一起收起和展开;横向尺寸必须限制在 compact surface 宽度内;历史记录底部必须为播放器高度和阴影预留间距,不能与播放器重叠;音量滑块展开到播放器外侧时要触发 geometry refresh
377377
17. 预览关闭时要清理 stale export error 和必要 preview lifecycle 状态,避免重新打开显示旧错误。
378378
18. 历史透明区域不能长期遮挡后方;可见气泡、按钮、预览控件和必要滚动区域可命中,气泡间透明区应尽量穿透。
379379
19. GalGame / ChoicePrompt 出现时,选项层在历史层上方。
@@ -581,7 +581,7 @@ Surface:
581581
7. 历史透明区不遮挡后方。
582582
8. 历史关闭动画结束后,历史面板卸载;关闭期间继续发生文字/语音对话时,历史区域不出现新气泡闪现。
583583
9. 历史重新展开后,关闭期间产生的新消息会按最新 `messages` 正常出现在历史中。
584-
10. 播放中的音乐栏在 compact 模式下停留在独立播放器挂载点;历史打开时显示,历史 closing / closed 时同步收起且不再命中;历史打开、关闭或卸载都不能把它挪回 composer,横向宽度不能突破 compact surface,历史记录不能贴住或覆盖播放器。
584+
10. 播放中的音乐栏在 compact 模式下停留在独立播放器挂载点;历史打开时显示,历史 closing / closed 时同步收起且不再命中;历史打开、关闭或卸载都不能把它挪回 composer,横向宽度不能突破 compact surface,历史记录不能贴住或覆盖播放器;音量滑块展开后可以点击和拖拽
585585
11. 预览关闭不会保留旧 error。
586586

587587
历史拖拽:

static/music_ui.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,14 @@
439439
|| node.getAttribute('data-music-player-mount') === 'compact-surface';
440440
}
441441

442+
function isCompactMusicGeometryMutationTarget(node) {
443+
if (!(node instanceof Element)) return false;
444+
const compactMusicMount = node.closest && node.closest('[data-music-player-mount="compact-surface"]');
445+
if (!compactMusicMount) return false;
446+
return node.classList.contains('music-bar-volume-container')
447+
|| node.classList.contains('music-bar-volume-slider-wrapper');
448+
}
449+
442450
function scheduleMusicBarRelocation(detachedMusicBar) {
443451
if (detachedMusicBar) pendingDetachedMusicBar = detachedMusicBar;
444452
if (musicMountRelocationFrame) return;
@@ -459,6 +467,10 @@
459467
scheduleMusicBarRelocation();
460468
return;
461469
}
470+
if (mutation.type === 'attributes' && isCompactMusicGeometryMutationTarget(mutation.target)) {
471+
requestCompactMusicGeometrySync();
472+
return;
473+
}
462474
for (const node of mutation.removedNodes) {
463475
const removedBar = findMusicBarInNode(node);
464476
if (removedBar) {
@@ -514,7 +526,10 @@
514526

515527
if (volumeBtn) volumeBtn.onclick = (e) => {
516528
e.preventDefault(); e.stopPropagation();
517-
if (volumeContainer) volumeContainer.classList.toggle('expanded');
529+
if (volumeContainer) {
530+
volumeContainer.classList.toggle('expanded');
531+
requestCompactMusicGeometrySync();
532+
}
518533
};
519534

520535
if (volumeSliderWrapper && volumeSlider) {
@@ -634,6 +649,7 @@
634649
const closeOnOutside = (e) => {
635650
if (volumeContainer.classList.contains('expanded') && !volumeContainer.contains(e.target)) {
636651
volumeContainer.classList.remove('expanded');
652+
requestCompactMusicGeometrySync();
637653
}
638654
};
639655
document.addEventListener('mousedown', closeOnOutside);
@@ -1747,6 +1763,7 @@
17471763
e.preventDefault();
17481764
e.stopPropagation();
17491765
volumeContainer.classList.toggle('expanded');
1766+
requestCompactMusicGeometrySync();
17501767
};
17511768

17521769
let isDraggingVolume = false;
@@ -1811,6 +1828,7 @@
18111828
const closeVolumeOnOutsideClick = (e) => {
18121829
if (volumeContainer.classList.contains('expanded') && !volumeContainer.contains(e.target)) {
18131830
volumeContainer.classList.remove('expanded');
1831+
requestCompactMusicGeometrySync();
18141832
}
18151833
};
18161834
addManagedListener('mousedown', closeVolumeOnOutsideClick);

tests/unit/test_react_chat_window_static.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -882,11 +882,24 @@ def test_compact_history_hit_contract_keeps_transparent_wrappers_out_of_hit_regi
882882
assert "document.getElementById('music-player-mount')" in music_ui_source
883883
assert "document.getElementById(MUSIC_CONFIG.dom.containerId)" in music_ui_source
884884
assert "mutation.type === 'attributes' && isMusicMountMutationTarget(mutation.target)" in music_ui_source
885+
assert "function isCompactMusicGeometryMutationTarget(node)" in music_ui_source
886+
assert "node.closest('[data-music-player-mount=\"compact-surface\"]')" in music_ui_source
887+
assert "node.classList.contains('music-bar-volume-container')" in music_ui_source
888+
assert "node.classList.contains('music-bar-volume-slider-wrapper')" in music_ui_source
889+
assert "mutation.type === 'attributes' && isCompactMusicGeometryMutationTarget(mutation.target)" in music_ui_source
885890
assert "attributes: true" in music_ui_source
886891
assert "'data-music-player-mount'" in music_ui_source
887892
assert "mountMusicBar(musicBar)" in music_ui_source
888893
assert "musicBar.setAttribute('data-compact-hit-region-id', 'music-player')" in music_ui_source
889894
assert "neko:compact-interaction-geometry-refresh" in music_ui_source
895+
volume_toggle_segments = music_ui_source.split("volumeContainer.classList.toggle('expanded');")
896+
assert len(volume_toggle_segments) == 3
897+
for segment in volume_toggle_segments[1:]:
898+
assert "requestCompactMusicGeometrySync();" in segment[:96]
899+
volume_close_segments = music_ui_source.split("volumeContainer.classList.remove('expanded');")
900+
assert len(volume_close_segments) == 3
901+
for segment in volume_close_segments[1:]:
902+
assert "requestCompactMusicGeometrySync();" in segment[:96]
890903
assert "window.addEventListener('neko:compact-interaction-geometry-refresh'" in script
891904
assert "kind === 'musicPlayer'" in script
892905
assert music_ui_source.count('data-compact-hit-region-id="music-player:volume"') == 2

0 commit comments

Comments
 (0)