Skip to content

Commit 70f36bd

Browse files
authored
添加请她离开后猫可以坐在聊天框上的机制 (#1622)
* 添加请她离开后猫可以坐在聊天框上的机制 * 优化猫坐到聊天框顶部的触发逻辑:降低自动靠近的吸附感,并在猫被甩下或手动抓下后,要求重新放到聊天框 100px 内才允许再次触发坐上去 * BroadcastChannel.postMessage 加了 try/catch,失败返回 false release timer 遇到已断开的 container 会提前返回 * 没有 compact surface 时:清掉 compactTopEdgeRearmRequired,不会把未来打开/移动的 compact 聊天框永久挡住
1 parent b128c01 commit 70f36bd

4 files changed

Lines changed: 1223 additions & 19 deletions

File tree

static/app-interpage.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1647,6 +1647,59 @@
16471647
var _yuiGuideChatFlushTimer = null;
16481648
var _yuiGuideChatFlushAttempts = 0;
16491649
var YUI_GUIDE_CHAT_FLUSH_MAX_ATTEMPTS = 50;
1650+
var IDLE_CHAT_COMPACT_SURFACE_HEARTBEAT_MS = 1000;
1651+
var idleChatCompactSurfaceHeartbeatTimer = 0;
1652+
var idleChatCompactSurfaceLastPayload = null;
1653+
1654+
function stopIdleChatCompactSurfaceHeartbeat() {
1655+
if (!idleChatCompactSurfaceHeartbeatTimer) return;
1656+
window.clearInterval(idleChatCompactSurfaceHeartbeatTimer);
1657+
idleChatCompactSurfaceHeartbeatTimer = 0;
1658+
}
1659+
1660+
function startIdleChatCompactSurfaceHeartbeat() {
1661+
if (idleChatCompactSurfaceHeartbeatTimer) return;
1662+
idleChatCompactSurfaceHeartbeatTimer = window.setInterval(function () {
1663+
if (!nekoBroadcastChannel ||
1664+
!idleChatCompactSurfaceLastPayload ||
1665+
!idleChatCompactSurfaceLastPayload.visible ||
1666+
!idleChatCompactSurfaceLastPayload.screenRect) {
1667+
stopIdleChatCompactSurfaceHeartbeat();
1668+
return;
1669+
}
1670+
nekoBroadcastChannel.postMessage(Object.assign({}, idleChatCompactSurfaceLastPayload, {
1671+
lanlan_name: getCurrentLanlanName(),
1672+
timestamp: Date.now(),
1673+
heartbeat: true
1674+
}));
1675+
}, IDLE_CHAT_COMPACT_SURFACE_HEARTBEAT_MS);
1676+
}
1677+
1678+
function syncIdleChatCompactSurfaceHeartbeat(payload) {
1679+
idleChatCompactSurfaceLastPayload = payload || null;
1680+
if (payload && payload.visible && payload.screenRect) {
1681+
startIdleChatCompactSurfaceHeartbeat();
1682+
return;
1683+
}
1684+
stopIdleChatCompactSurfaceHeartbeat();
1685+
}
1686+
1687+
function postIdleChatCompactSurfaceState(detail) {
1688+
if (!nekoBroadcastChannel) return;
1689+
var screenRect = detail && detail.screenRect ? detail.screenRect : null;
1690+
var payload = {
1691+
action: 'idle_chat_compact_surface_state',
1692+
source: 'chat-window',
1693+
lanlan_name: getCurrentLanlanName(),
1694+
visible: !!screenRect,
1695+
screenRect: screenRect,
1696+
resizeActive: !!(detail && detail.resizeActive),
1697+
dragging: !!(detail && detail.dragging),
1698+
timestamp: Date.now()
1699+
};
1700+
nekoBroadcastChannel.postMessage(payload);
1701+
syncIdleChatCompactSurfaceHeartbeat(payload);
1702+
}
16501703

16511704
function scheduleYuiGuideChatMessageFlush(delay) {
16521705
if (_yuiGuideChatFlushTimer) return;
@@ -1820,6 +1873,18 @@
18201873
dispatchIdleChatMinimizedState(event.data);
18211874
break;
18221875
}
1876+
case 'idle_chat_compact_surface_state': {
1877+
var compactSurfaceCurrentName = getCurrentLanlanName();
1878+
if (event.data.lanlan_name && (!compactSurfaceCurrentName || event.data.lanlan_name !== compactSurfaceCurrentName)) break;
1879+
dispatchIdleChatCompactSurfaceState(event.data);
1880+
break;
1881+
}
1882+
case 'idle_cat1_compact_mirror_state': {
1883+
var cat1MirrorCurrentName = getCurrentLanlanName();
1884+
if (event.data.lanlan_name && (!cat1MirrorCurrentName || event.data.lanlan_name !== cat1MirrorCurrentName)) break;
1885+
dispatchIdleCat1CompactMirrorState(event.data);
1886+
break;
1887+
}
18231888
case 'idle_chat_pair_move_bounds': {
18241889
var pairMoveChatCurrentName = getCurrentLanlanName();
18251890
if (event.data.lanlan_name && (!pairMoveChatCurrentName || event.data.lanlan_name !== pairMoveChatCurrentName)) break;
@@ -2113,6 +2178,40 @@
21132178
}));
21142179
}
21152180

2181+
function dispatchIdleChatCompactSurfaceState(detail) {
2182+
window.dispatchEvent(new CustomEvent('neko:idle-chat-compact-surface-state', {
2183+
detail: Object.assign({
2184+
action: 'idle_chat_compact_surface_state',
2185+
source: '',
2186+
reason: '',
2187+
visible: false,
2188+
screenRect: null,
2189+
timestamp: Date.now(),
2190+
via: 'broadcast-channel'
2191+
}, detail || {}, {
2192+
via: 'broadcast-channel'
2193+
})
2194+
}));
2195+
}
2196+
2197+
function dispatchIdleCat1CompactMirrorState(detail) {
2198+
window.dispatchEvent(new CustomEvent('neko:idle-cat1-compact-mirror-state', {
2199+
detail: Object.assign({
2200+
action: 'idle_cat1_compact_mirror_state',
2201+
source: '',
2202+
reason: '',
2203+
active: false,
2204+
surfaceScreenRect: null,
2205+
anchorRatio: null,
2206+
catRect: null,
2207+
timestamp: Date.now(),
2208+
via: 'broadcast-channel'
2209+
}, detail || {}, {
2210+
via: 'broadcast-channel'
2211+
})
2212+
}));
2213+
}
2214+
21162215
function dispatchIdleChatPairMoveBounds(detail) {
21172216
window.dispatchEvent(new CustomEvent('neko:idle-chat-pair-move-bounds', {
21182217
detail: Object.assign({
@@ -2308,6 +2407,11 @@
23082407
}, detail));
23092408
});
23102409

2410+
window.addEventListener('neko:compact-surface-layout-change', function (evt) {
2411+
var detail = evt && evt.detail && typeof evt.detail === 'object' ? evt.detail : null;
2412+
postIdleChatCompactSurfaceState(detail);
2413+
});
2414+
23112415
// Chat 窗口初始化时,向 Pet 窗口请求当前已缓存的头像
23122416
if (isStandaloneChatPage() && nekoBroadcastChannel) {
23132417
var initialLanlanName = (window.lanlan_config && window.lanlan_config.lanlan_name) || '';

static/app-react-chat-window.js

Lines changed: 176 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,11 @@
261261
var compactSurfacePendingModelOpen = false;
262262
var compactSurfaceResizeSession = null;
263263
var compactSurfaceDesktopResizeActive = false;
264+
var compactSurfaceDesktopDragActive = false;
265+
var IDLE_CAT1_COMPACT_MIRROR_TIMEOUT_MS = 1600;
266+
var idleCat1CompactMirrorElement = null;
267+
var idleCat1CompactMirrorTimer = 0;
268+
var idleCat1CompactMirrorLastDetail = null;
264269

265270
function normalizeCompactDesktopRect(raw) {
266271
if (!raw) return null;
@@ -306,6 +311,7 @@
306311
}
307312

308313
function handleDesktopCompactLayoutChange(layout) {
314+
compactSurfaceDesktopDragActive = !!(layout && layout.dragging);
309315
var nextAnchorSnapshot = getCompactDesktopLayoutAnchorSnapshot(layout);
310316
var baseAnchorChanged = false;
311317
if (!nextAnchorSnapshot) {
@@ -641,6 +647,142 @@
641647
};
642648
}
643649

650+
function getIdleCat1CompactMirrorElement() {
651+
if (idleCat1CompactMirrorElement && idleCat1CompactMirrorElement.isConnected) {
652+
return idleCat1CompactMirrorElement;
653+
}
654+
var host = document.body;
655+
if (!host) return null;
656+
var element = document.createElement('div');
657+
element.id = 'neko-idle-cat1-compact-mirror';
658+
element.className = 'neko-idle-cat1-compact-mirror';
659+
element.setAttribute('data-compact-geometry-owner', 'surface');
660+
element.setAttribute('data-compact-geometry-item', 'cat1Mirror');
661+
element.setAttribute('aria-hidden', 'true');
662+
element.hidden = true;
663+
var image = document.createElement('img');
664+
image.className = 'neko-idle-cat1-compact-mirror-art';
665+
image.alt = '';
666+
image.draggable = false;
667+
element.appendChild(image);
668+
host.appendChild(element);
669+
idleCat1CompactMirrorElement = element;
670+
return element;
671+
}
672+
673+
function clearIdleCat1CompactMirrorTimer() {
674+
if (!idleCat1CompactMirrorTimer) return;
675+
window.clearTimeout(idleCat1CompactMirrorTimer);
676+
idleCat1CompactMirrorTimer = 0;
677+
}
678+
679+
function hideIdleCat1CompactMirror(reason) {
680+
clearIdleCat1CompactMirrorTimer();
681+
var element = idleCat1CompactMirrorElement;
682+
if (element) {
683+
element.hidden = true;
684+
element.removeAttribute('data-active');
685+
element.style.removeProperty('left');
686+
element.style.removeProperty('top');
687+
element.style.removeProperty('width');
688+
element.style.removeProperty('height');
689+
}
690+
idleCat1CompactMirrorLastDetail = null;
691+
scheduleCompactMinimizeBallTracking();
692+
syncCompactInteractionGeometry();
693+
}
694+
695+
function getIdleCat1CompactMirrorWindowBounds() {
696+
var windowBounds = getCompactSurfaceDesktopWindowBounds();
697+
if (windowBounds) return windowBounds;
698+
var x = Number(window.screenX);
699+
var y = Number(window.screenY);
700+
return {
701+
x: Number.isFinite(x) ? x : 0,
702+
y: Number.isFinite(y) ? y : 0
703+
};
704+
}
705+
706+
function getIdleCat1CompactMirrorPageRect(detail) {
707+
var surface = normalizeCompactDesktopRect(detail && detail.surfaceScreenRect);
708+
if (!surface) return null;
709+
var catRect = detail && detail.catRect ? detail.catRect : null;
710+
var catWidth = Math.round(Number(catRect && catRect.width) || 112);
711+
var catHeight = Math.round(Number(catRect && catRect.height) || 112);
712+
if (catWidth <= 0 || catHeight <= 0) return null;
713+
var windowBounds = getIdleCat1CompactMirrorWindowBounds();
714+
var surfaceLeft = surface.left - (Number(windowBounds.x) || 0);
715+
var surfaceTop = surface.top - (Number(windowBounds.y) || 0);
716+
var sidePadding = Math.max(0, Number(detail && detail.sidePaddingPx) || 12);
717+
var capInset = Math.max(sidePadding, surface.height / 2 + sidePadding);
718+
var edgePadding = Math.min(capInset, Math.max(0, surface.width / 2));
719+
var minCenterX = surfaceLeft + edgePadding;
720+
var maxCenterX = surfaceLeft + surface.width - edgePadding;
721+
var ratio = Number(detail && detail.anchorRatio);
722+
if (!Number.isFinite(ratio)) ratio = 0.5;
723+
ratio = Math.max(0, Math.min(1, ratio));
724+
var centerX = maxCenterX >= minCenterX
725+
? minCenterX + (maxCenterX - minCenterX) * ratio
726+
: surfaceLeft + surface.width / 2;
727+
var overlap = Number(detail && detail.overlapPx);
728+
if (!Number.isFinite(overlap)) overlap = 28;
729+
return {
730+
left: Math.round(centerX - catWidth / 2),
731+
top: Math.round(surfaceTop - catHeight + overlap),
732+
width: catWidth,
733+
height: catHeight
734+
};
735+
}
736+
737+
function showIdleCat1CompactMirror(detail) {
738+
var element = getIdleCat1CompactMirrorElement();
739+
var rect = getIdleCat1CompactMirrorPageRect(detail);
740+
if (!element || !rect) {
741+
hideIdleCat1CompactMirror('invalid');
742+
return;
743+
}
744+
idleCat1CompactMirrorLastDetail = Object.assign({}, detail || {});
745+
var image = element.querySelector('.neko-idle-cat1-compact-mirror-art');
746+
if (image) {
747+
var src = detail && detail.assetUrl ? String(detail.assetUrl) : '/static/assets/neko-idle/cat-idle-cat1.gif';
748+
if (image.getAttribute('src') !== src) image.setAttribute('src', src);
749+
image.style.transform = detail && detail.facingRight ? 'scaleX(-1)' : 'scaleX(1)';
750+
}
751+
element.style.left = rect.left + 'px';
752+
element.style.top = rect.top + 'px';
753+
element.style.width = rect.width + 'px';
754+
element.style.height = rect.height + 'px';
755+
element.hidden = false;
756+
element.setAttribute('data-active', 'true');
757+
clearIdleCat1CompactMirrorTimer();
758+
idleCat1CompactMirrorTimer = window.setTimeout(function () {
759+
hideIdleCat1CompactMirror('timeout');
760+
}, IDLE_CAT1_COMPACT_MIRROR_TIMEOUT_MS);
761+
scheduleCompactMinimizeBallTracking();
762+
syncCompactInteractionGeometry();
763+
}
764+
765+
function refreshIdleCat1CompactMirrorPosition() {
766+
var element = idleCat1CompactMirrorElement;
767+
if (!element || element.hidden || !idleCat1CompactMirrorLastDetail) return;
768+
var rect = getIdleCat1CompactMirrorPageRect(idleCat1CompactMirrorLastDetail);
769+
if (!rect) return;
770+
element.style.left = rect.left + 'px';
771+
element.style.top = rect.top + 'px';
772+
element.style.width = rect.width + 'px';
773+
element.style.height = rect.height + 'px';
774+
syncCompactInteractionGeometry();
775+
}
776+
777+
function handleIdleCat1CompactMirrorState(event) {
778+
var detail = event && event.detail && typeof event.detail === 'object' ? event.detail : null;
779+
if (!detail || !detail.active) {
780+
hideIdleCat1CompactMirror(detail && detail.reason ? detail.reason : 'inactive');
781+
return;
782+
}
783+
showIdleCat1CompactMirror(detail);
784+
}
785+
644786
function dispatchCompactSurfaceLayoutChange(rect) {
645787
var detail = rect || null;
646788
if (detail && isElectronChatWindow()) {
@@ -649,6 +791,11 @@
649791
resizeActive: !!compactSurfaceResizeSession
650792
});
651793
}
794+
if (detail) {
795+
detail = Object.assign({}, detail, {
796+
dragging: !!(dragState && dragState.compactSurface) || compactSurfaceDesktopDragActive
797+
});
798+
}
652799
window.dispatchEvent(new CustomEvent('neko:compact-surface-layout-change', {
653800
detail: detail
654801
}));
@@ -1018,7 +1165,7 @@
10181165
var item = element.getAttribute('data-compact-geometry-item') || '';
10191166
if (item === 'choice' && element.getAttribute('data-choice-layer-open') !== 'true') return false;
10201167
if (item === 'toolFan' && element.getAttribute('data-compact-input-tool-fan-open') !== 'true') return false;
1021-
if (element.getAttribute('aria-hidden') === 'true' && item !== 'resizeHandle') return false;
1168+
if (element.getAttribute('aria-hidden') === 'true' && item !== 'resizeHandle' && item !== 'cat1Mirror') return false;
10221169
var style = window.getComputedStyle ? window.getComputedStyle(element) : null;
10231170
if (style && (style.display === 'none' || style.visibility === 'hidden')) return false;
10241171
return true;
@@ -1275,12 +1422,28 @@
12751422
!root.contains(element)
12761423
&& !element.classList.contains('compact-input-tool-fan')
12771424
&& !element.classList.contains('compact-chat-choice-anchor')
1425+
&& !element.classList.contains('neko-idle-cat1-compact-mirror')
12781426
) return items;
12791427
if (!shouldIncludeCompactGeometryElement(element)) return items;
12801428
var compactGeometryItem = element.getAttribute('data-compact-geometry-item');
12811429
if (compactGeometryItem === 'toolFan') {
12821430
return items.concat(collectCompactToolFanGeometryItems(element));
12831431
}
1432+
if (compactGeometryItem === 'cat1Mirror') {
1433+
var mirrorRect = getCompactGeometryElementRect(element);
1434+
if (mirrorRect) {
1435+
items.push({
1436+
id: 'cat1Mirror:native',
1437+
owner: 'surface',
1438+
kind: 'cat1Mirror',
1439+
visualRect: mirrorRect,
1440+
hitRect: null,
1441+
nativeRect: mirrorRect,
1442+
interactive: false
1443+
});
1444+
}
1445+
return items;
1446+
}
12841447
if (element.getAttribute('data-compact-geometry-hit-scope') === 'children') {
12851448
return items.concat(collectCompactCompositeGeometryItems(element, compactGeometryItem));
12861449
}
@@ -1409,7 +1572,8 @@
14091572
Math.round(target.left),
14101573
Math.round(target.top),
14111574
Math.round(target.width),
1412-
Math.round(target.height || COMPACT_SURFACE_DEFAULT_HEIGHT)
1575+
Math.round(target.height || COMPACT_SURFACE_DEFAULT_HEIGHT),
1576+
(!!(dragState && dragState.compactSurface) || compactSurfaceDesktopDragActive) ? 'dragging' : 'idle'
14131577
].join(':');
14141578
if (snapshot === compactSurfaceAnchorSnapshot) {
14151579
return;
@@ -4746,6 +4910,7 @@
47464910
cancelActiveAnimation(); // 清理进行中的折叠/展开回调
47474911
clearIdleDockState();
47484912
deactivateToolCursor();
4913+
hideIdleCat1CompactMirror('close-window');
47494914

47504915
// 如果当前处于最小化状态,恢复 shell 到正常态
47514916
if (minimized) {
@@ -4860,6 +5025,7 @@
48605025
var changedTouch = opts.changedTouches && opts.changedTouches.length > 0 ? opts.changedTouches[0] : null;
48615026

48625027
var wasMoved = dragState.moved;
5028+
var wasCompactSurface = !!dragState.compactSurface;
48635029

48645030
var shell = getShell();
48655031
if (shell) {
@@ -4872,6 +5038,12 @@
48725038

48735039
dragState = null;
48745040
document.body.classList.remove('react-chat-window-dragging');
5041+
if (wasCompactSurface && wasMoved) {
5042+
var compactRect = getCurrentCompactSurfaceRect();
5043+
if (compactRect) {
5044+
dispatchCompactSurfaceLayoutChange(compactRect);
5045+
}
5046+
}
48755047

48765048
// 移动过的拖拽不该再变成点击:吞掉 mouseup 落点补发的那一次 click。
48775049
if (wasMoved) {
@@ -5452,6 +5624,7 @@
54525624
if (!detail) return;
54535625
scheduleElectronCat1PairMoveBounds(detail.screenRect || detail.bounds);
54545626
});
5627+
window.addEventListener('neko:idle-cat1-compact-mirror-state', handleIdleCat1CompactMirrorState);
54555628
window.addEventListener('live2d-return-click', function () {
54565629
if (hasElectronIdleDockPendingOrActive()) { exitElectronIdleDock(); }
54575630
if (hasIdleDockPendingOrActive()) { exitIdleDock(); return; }
@@ -5471,6 +5644,7 @@
54715644
window.addEventListener('neko:desktop-compact-layout-change', function (event) {
54725645
var layout = event && event.detail ? event.detail : window.__nekoDesktopCompactLayout;
54735646
handleDesktopCompactLayoutChange(layout || null);
5647+
refreshIdleCat1CompactMirrorPosition();
54745648
});
54755649
if (window.__nekoDesktopCompactLayout) {
54765650
handleDesktopCompactLayoutChange(window.__nekoDesktopCompactLayout);

0 commit comments

Comments
 (0)