Skip to content

Commit d5c8e0c

Browse files
authored
[codex] suppress avatar toolbar outside guided steps (#2091)
* fix(tutorial): suppress avatar toolbar outside guided steps * refactor(tutorial): share guide persistence helpers * fix(pngtuber): refresh toolbar after guide suppression * fix(tutorial): keep day4 toolbar and mobile locks synced * fix(tutorial): refresh avatar toolbar suppression immediately
1 parent 4c64ec6 commit d5c8e0c

9 files changed

Lines changed: 354 additions & 49 deletions

File tree

main_routers/pages_router.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@
7070
_PROJECT_ROOT / "static/avatar-performance-stage.js",
7171
_PROJECT_ROOT / "static/live2d-interaction.js",
7272
_PROJECT_ROOT / "static/live2d-init.js",
73+
_PROJECT_ROOT / "static/live2d-ui-buttons.js",
74+
_PROJECT_ROOT / "static/vrm-ui-buttons.js",
75+
_PROJECT_ROOT / "static/mmd-ui-buttons.js",
76+
_PROJECT_ROOT / "static/pngtuber-core.js",
7377
_PROJECT_ROOT / "static/i18n-i18next.js",
7478
_PROJECT_ROOT / "static/app-auto-goodbye.js",
7579
_PROJECT_ROOT / "static/app-interpage.js",

static/avatar-ui-buttons.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,17 @@
99
// 浮动按钮入场动画(错位级联滑入 + 淡入;从上往下)。
1010
// 退场不做动画 —— 直接 display:none,因为浏览器在 microtask 拦截前已 commit
1111
// display:none 到下一帧渲染流程,可靠的退场需要改大量调用点,权衡之下放弃。
12+
function isNekoYuiGuideFloatingToolbarSuppressed() {
13+
return !!(
14+
typeof window !== 'undefined'
15+
&& window.nekoYuiGuideFloatingToolbarSuppressed === true
16+
);
17+
}
18+
19+
if (typeof window !== 'undefined') {
20+
window.isNekoYuiGuideFloatingToolbarSuppressed = isNekoYuiGuideFloatingToolbarSuppressed;
21+
}
22+
1223
function _ensureFloatingButtonsAnimationStyles() {
1324
if (document.getElementById('neko-floating-buttons-animation-styles')) return;
1425
const style = document.createElement('style');

static/common-ui-hud.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const STANDALONE_HUD_POSITION = Object.freeze({
1212
right: 'auto',
1313
transform: 'none'
1414
});
15+
const AGENT_TASK_HUD_COLLAPSED_KEY = 'agent-task-hud-collapsed-v2';
1516

1617
function appendPluginDashboardOpenerOrigin(url) {
1718
const target = new URL(url, document.baseURI || window.location.href);
@@ -655,7 +656,6 @@ window.AgentHUD.createAgentTaskHUD = function () {
655656
});
656657

657658
// 整体折叠逻辑 (key v2: reset stale collapsed state)
658-
const hudCollapsedKey = 'agent-task-hud-collapsed-v2';
659659
const applyHudCollapsed = (collapsed) => {
660660
if (!collapsed && hud.style.display !== 'none') {
661661
// Check edge collision for smooth unfolding direction towards the left
@@ -713,16 +713,22 @@ window.AgentHUD.createAgentTaskHUD = function () {
713713

714714
// Default: expanded
715715
let hudCollapsed = false;
716-
try { hudCollapsed = localStorage.getItem(hudCollapsedKey) === 'true'; } catch (_) { }
716+
try { hudCollapsed = localStorage.getItem(AGENT_TASK_HUD_COLLAPSED_KEY) === 'true'; } catch (_) { }
717717
applyHudCollapsed(hudCollapsed);
718718

719719
minimizeBtn.addEventListener('click', (e) => {
720720
e.stopPropagation();
721721
hudCollapsed = !hudCollapsed;
722722
applyHudCollapsed(hudCollapsed);
723-
try { localStorage.setItem(hudCollapsedKey, String(hudCollapsed)); } catch (_) { }
723+
try { localStorage.setItem(AGENT_TASK_HUD_COLLAPSED_KEY, String(hudCollapsed)); } catch (_) { }
724724
});
725725

726+
hud._setAgentTaskHudCollapsed = (collapsed) => {
727+
hudCollapsed = collapsed === true;
728+
applyHudCollapsed(hudCollapsed);
729+
try { localStorage.setItem(AGENT_TASK_HUD_COLLAPSED_KEY, String(hudCollapsed)); } catch (_) { }
730+
};
731+
726732
// 空状态提示
727733
const emptyState = document.createElement('div');
728734
emptyState.id = 'agent-task-empty';
@@ -802,6 +808,17 @@ window.AgentHUD.showAgentTaskHUD = function () {
802808
};
803809

804810
// 隐藏任务 HUD
811+
window.AgentHUD.expandAgentTaskHUD = function () {
812+
const hud = document.getElementById('agent-task-hud');
813+
if (!hud) return false;
814+
if (typeof hud._setAgentTaskHudCollapsed === 'function') {
815+
hud._setAgentTaskHudCollapsed(false);
816+
return true;
817+
}
818+
try { localStorage.setItem(AGENT_TASK_HUD_COLLAPSED_KEY, 'false'); } catch (_) {}
819+
return false;
820+
};
821+
805822
window.AgentHUD.hideAgentTaskHUD = function () {
806823
const hud = document.getElementById('agent-task-hud');
807824
if (!hud) {

static/live2d-ui-buttons.js

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,18 @@ function isYuiGuideLive2DPreparing() {
4343
);
4444
}
4545

46+
function isYuiGuideFloatingToolbarSuppressed() {
47+
return !!(
48+
window.isNekoYuiGuideFloatingToolbarSuppressed
49+
&& window.isNekoYuiGuideFloatingToolbarSuppressed()
50+
);
51+
}
52+
4653
function hideYuiGuideLive2DPreparingButtonStyles(buttonsContainer) {
4754
if (!buttonsContainer || !buttonsContainer.style || typeof buttonsContainer.style.removeProperty !== 'function') {
4855
return;
4956
}
57+
buttonsContainer.dataset.yuiGuideForcedHidden = 'true';
5058
buttonsContainer.style.setProperty('display', 'none', 'important');
5159
buttonsContainer.style.setProperty('visibility', 'hidden', 'important');
5260
buttonsContainer.style.setProperty('opacity', '0', 'important');
@@ -57,10 +65,15 @@ function restoreYuiGuideLive2DPreparingButtonStyles(buttonsContainer) {
5765
if (!buttonsContainer || !buttonsContainer.style || typeof buttonsContainer.style.removeProperty !== 'function') {
5866
return;
5967
}
60-
buttonsContainer.style.removeProperty('display');
61-
buttonsContainer.style.removeProperty('visibility');
62-
buttonsContainer.style.removeProperty('opacity');
63-
buttonsContainer.style.removeProperty('pointer-events');
68+
const forcedHidden = buttonsContainer.dataset.yuiGuideForcedHidden === 'true';
69+
if (!forcedHidden) {
70+
return;
71+
}
72+
const forcedProperties = ['display', 'visibility', 'opacity', 'pointer-events'];
73+
forcedProperties.forEach((property) => {
74+
buttonsContainer.style.removeProperty(property);
75+
});
76+
delete buttonsContainer.dataset.yuiGuideForcedHidden;
6477
}
6578

6679
/**
@@ -180,6 +193,17 @@ Live2DManager.prototype.setupHTMLLockIcon = function(model) {
180193
if (shouldSkipLive2DUiTick(this, '_x11LockIconLastTickAt', LIVE2D_X11_UI_TICK_MS)) {
181194
return;
182195
}
196+
if (isYuiGuideFloatingToolbarSuppressed()) {
197+
lockIcon.dataset.yuiGuideForcedHidden = 'true';
198+
lockIcon.style.visibility = 'hidden';
199+
lockIcon.style.opacity = '0';
200+
return;
201+
}
202+
if (lockIcon.dataset.yuiGuideForcedHidden === 'true') {
203+
delete lockIcon.dataset.yuiGuideForcedHidden;
204+
lockIcon.style.visibility = '';
205+
lockIcon.style.opacity = '';
206+
}
183207
if (!model || !model.parent) {
184208
// 教程期间不隐藏锁图标,防止高亮框位置被刷到 (0,0)
185209
if (lockIcon && !window.isInTutorial) lockIcon.style.display = 'none';
@@ -596,6 +620,10 @@ Live2DManager.prototype.setupFloatingButtons = function(model) {
596620
if (shouldSkipLive2DUiTick(this, '_x11FloatingButtonsLastTickAt', LIVE2D_X11_UI_TICK_MS)) {
597621
return;
598622
}
623+
if (isYuiGuideFloatingToolbarSuppressed()) {
624+
hideYuiGuideLive2DPreparingButtonStyles(buttonsContainer);
625+
return;
626+
}
599627
if (!model || !model.parent) {
600628
return;
601629
}
@@ -650,7 +678,7 @@ Live2DManager.prototype.setupFloatingButtons = function(model) {
650678
if (this.isLocked) {
651679
return;
652680
}
653-
if (isYuiGuideLive2DPreparing()) {
681+
if (isYuiGuideLive2DPreparing() || isYuiGuideFloatingToolbarSuppressed()) {
654682
hideYuiGuideLive2DPreparingButtonStyles(buttonsContainer);
655683
return;
656684
}
@@ -662,7 +690,7 @@ Live2DManager.prototype.setupFloatingButtons = function(model) {
662690
if (!this.isFocusing && !inTutorial) {
663691
buttonsContainer.style.display = 'none';
664692
} else if (inTutorial) {
665-
if (isYuiGuideLive2DPreparing()) {
693+
if (isYuiGuideLive2DPreparing() || isYuiGuideFloatingToolbarSuppressed()) {
666694
hideYuiGuideLive2DPreparingButtonStyles(buttonsContainer);
667695
} else {
668696
restoreYuiGuideLive2DPreparingButtonStyles(buttonsContainer);
@@ -679,10 +707,11 @@ Live2DManager.prototype.setupFloatingButtons = function(model) {
679707

680708
this.tutorialProtectionTimer = setInterval(() => {
681709
if (window.isInTutorial === true) {
682-
if (isYuiGuideLive2DPreparing()) {
710+
if (isYuiGuideLive2DPreparing() || isYuiGuideFloatingToolbarSuppressed()) {
683711
hideYuiGuideLive2DPreparingButtonStyles(buttonsContainer);
684712
return;
685713
}
714+
restoreYuiGuideLive2DPreparingButtonStyles(buttonsContainer);
686715
const style = window.getComputedStyle(buttonsContainer);
687716
if (style.display === 'none') {
688717
buttonsContainer.style.setProperty('display', 'flex', 'important');

static/mmd-ui-buttons.js

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,13 @@ AvatarButtonMixin.apply(MMDManager.prototype, 'mmd', {
1717
returnBreathingStyleId: 'mmd-return-button-breathing-styles'
1818
});
1919

20+
function isYuiGuideFloatingToolbarSuppressed() {
21+
return !!(
22+
window.isNekoYuiGuideFloatingToolbarSuppressed
23+
&& window.isNekoYuiGuideFloatingToolbarSuppressed()
24+
);
25+
}
26+
2027
/**
2128
* 设置浮动按钮系统(MMD 特定)
2229
*/
@@ -41,6 +48,14 @@ MMDManager.prototype.setupFloatingButtons = function() {
4148

4249
// MMD 特定的响应式布局处理
4350
const applyResponsiveFloatingLayout = () => {
51+
if (isYuiGuideFloatingToolbarSuppressed()) {
52+
buttonsContainer.style.display = 'none';
53+
buttonsContainer.style.visibility = 'hidden';
54+
buttonsContainer.style.opacity = '0';
55+
return;
56+
}
57+
buttonsContainer.style.removeProperty('visibility');
58+
buttonsContainer.style.removeProperty('opacity');
4459
if (this._isInReturnState) { buttonsContainer.style.display = 'none'; return; }
4560
const isLocked = this.isLocked;
4661
if (isLocked) { buttonsContainer.style.display = 'none'; return; }
@@ -63,6 +78,7 @@ MMDManager.prototype.setupFloatingButtons = function() {
6378

6479
// 锁图标显示逻辑
6580
const shouldShowLockIcon = () => {
81+
if (isYuiGuideFloatingToolbarSuppressed()) return false;
6682
// 教程期间始终显示锁图标,防止高亮框位置异常
6783
if (window.isInTutorial) return true;
6884
const isLocked = this.isLocked;
@@ -414,6 +430,25 @@ MMDManager.prototype.setupFloatingButtons = function() {
414430
lockIcon.addEventListener('mousedown', toggleLock);
415431
lockIcon.addEventListener('touchstart', toggleLock, { passive: false });
416432

433+
const suppressionChangeHandler = () => {
434+
requestAnimationFrame(() => {
435+
applyResponsiveFloatingLayout();
436+
const currentLockIcon = this._mmdLockIcon;
437+
if (currentLockIcon && !this._isInReturnState) {
438+
const shouldShowLock = !!(this._shouldShowMmdLockIcon && this._shouldShowMmdLockIcon());
439+
currentLockIcon.style.display = shouldShowLock ? 'block' : 'none';
440+
currentLockIcon.style.visibility = shouldShowLock ? 'visible' : 'hidden';
441+
currentLockIcon.style.opacity = shouldShowLock ? '' : '0';
442+
}
443+
});
444+
};
445+
this._uiWindowHandlers.push({
446+
event: 'neko:yui-guide-floating-toolbar-suppression-change',
447+
handler: suppressionChangeHandler,
448+
target: window
449+
});
450+
window.addEventListener('neko:yui-guide-floating-toolbar-suppression-change', suppressionChangeHandler);
451+
417452
// 启动 UI 更新循环
418453
this._startUIUpdateLoop();
419454

@@ -707,9 +742,16 @@ MMDManager.prototype._startUIUpdateLoop = function() {
707742
if (buttonsContainer) {
708743
if (isMobile) {
709744
buttonsContainer.style.transformOrigin = 'right bottom';
710-
buttonsContainer.style.visibility = 'visible';
711-
buttonsContainer.style.opacity = '1';
712-
buttonsContainer.style.display = this.isLocked ? 'none' : 'flex';
745+
const suppressed = isYuiGuideFloatingToolbarSuppressed();
746+
buttonsContainer.style.visibility = suppressed ? 'hidden' : 'visible';
747+
buttonsContainer.style.opacity = suppressed ? '0' : '1';
748+
buttonsContainer.style.display = suppressed || this.isLocked ? 'none' : 'flex';
749+
if (lockIcon && !this._isInReturnState) {
750+
const shouldShowLock = !!(this._shouldShowMmdLockIcon && this._shouldShowMmdLockIcon());
751+
lockIcon.style.display = shouldShowLock ? 'block' : 'none';
752+
lockIcon.style.visibility = shouldShowLock ? 'visible' : 'hidden';
753+
lockIcon.style.opacity = shouldShowLock ? '' : '0';
754+
}
713755
} else {
714756
buttonsContainer.style.transformOrigin = 'left top';
715757
const screenWidth = window.innerWidth;
@@ -763,7 +805,7 @@ MMDManager.prototype._startUIUpdateLoop = function() {
763805
Number.isFinite(parseFloat(buttonsContainer.style.left)) &&
764806
Number.isFinite(parseFloat(buttonsContainer.style.top)) &&
765807
!this._snapUIPosition;
766-
const shouldShowButtons = isUiPositionReady &&
808+
const shouldShowButtons = !isYuiGuideFloatingToolbarSuppressed() && isUiPositionReady &&
767809
(inTutorial || (!isLocked && (this._mmdUiNearModel || hoveringButtons || hasOpenOverlay)));
768810
buttonsContainer.style.display = shouldShowButtons ? 'flex' : 'none';
769811
buttonsContainer.style.visibility = shouldShowButtons ? 'visible' : 'hidden';

static/pngtuber-core.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -941,7 +941,7 @@
941941
this.container.classList.remove('locked-hover-fade');
942942
}
943943
if (updateFloatingButtons && this._floatingButtonsContainer) {
944-
this._floatingButtonsContainer.style.display = this.isLocked ? 'none' : 'flex';
944+
this._floatingButtonsContainer.style.display = this.isLocked || isYuiGuideFloatingToolbarSuppressed() ? 'none' : 'flex';
945945
}
946946
if (typeof this.updateLockIconPosition === 'function') {
947947
this.updateLockIconPosition();
@@ -1196,6 +1196,12 @@
11961196
updateLockIconPosition() {
11971197
const lockIcon = this._lockIconElement || document.getElementById('pngtuber-lock-icon');
11981198
if (!lockIcon) return;
1199+
if (isYuiGuideFloatingToolbarSuppressed()) {
1200+
lockIcon.style.display = 'none';
1201+
lockIcon.style.visibility = 'hidden';
1202+
lockIcon.style.opacity = '0';
1203+
return;
1204+
}
11991205
const image = this.image || (this.ensureContainer() && this.image);
12001206
const rect = image ? image.getBoundingClientRect() : null;
12011207
if (!rect || rect.width <= 0 || rect.height <= 0) {
@@ -1209,6 +1215,7 @@
12091215
lockIcon.style.left = `${Math.max(0, Math.min(targetX, window.innerWidth - 40))}px`;
12101216
lockIcon.style.top = `${Math.max(0, Math.min(targetY, window.innerHeight - 40))}px`;
12111217
lockIcon.style.display = 'block';
1218+
lockIcon.style.visibility = 'visible';
12121219

12131220
const lockRect = lockIcon.getBoundingClientRect();
12141221
let isOverlapped = false;
@@ -1831,6 +1838,13 @@
18311838
PNGTuberManager.prototype._pngtuberAvatarUiApplied = true;
18321839
}
18331840

1841+
function isYuiGuideFloatingToolbarSuppressed() {
1842+
return !!(
1843+
window.isNekoYuiGuideFloatingToolbarSuppressed
1844+
&& window.isNekoYuiGuideFloatingToolbarSuppressed()
1845+
);
1846+
}
1847+
18341848
function installPNGTuberFloatingButtons() {
18351849
applyPNGTuberAvatarUiMixins();
18361850
if (typeof PNGTuberManager.prototype.setupFloatingButtonsBase !== 'function') return;
@@ -1846,6 +1860,13 @@
18461860
this._buttonConfigs = this.getDefaultButtonConfigs();
18471861

18481862
this.updateFloatingButtonsPosition = () => {
1863+
if (isYuiGuideFloatingToolbarSuppressed()) {
1864+
buttonsContainer.style.display = 'none';
1865+
buttonsContainer.style.visibility = 'hidden';
1866+
buttonsContainer.style.opacity = '0';
1867+
this.updateLockIconPosition();
1868+
return;
1869+
}
18491870
if (this._isInReturnState) {
18501871
buttonsContainer.style.display = 'none';
18511872
return;
@@ -2011,8 +2032,10 @@
20112032
});
20122033
this._uiWindowHandlers.push({ event: 'resize', handler: scheduleLayout, target: window });
20132034
this._uiWindowHandlers.push({ event: 'orientationchange', handler: scheduleLayout, target: window });
2035+
this._uiWindowHandlers.push({ event: 'neko:yui-guide-floating-toolbar-suppression-change', handler: scheduleLayout, target: window });
20142036
window.addEventListener('resize', scheduleLayout);
20152037
window.addEventListener('orientationchange', scheduleLayout);
2038+
window.addEventListener('neko:yui-guide-floating-toolbar-suppression-change', scheduleLayout);
20162039
if (this.image) {
20172040
this.image.addEventListener('load', scheduleLayout);
20182041
this._uiWindowHandlers.push({ event: 'load', handler: scheduleLayout, target: this.image });

0 commit comments

Comments
 (0)