Files
DemonGatanjieuandClaude Opus 5.5 7da4e1d91e fix(audio): the audio guide's tour walks the audio page; suggested folder layout
The "!" guide on the audio page hid its tour banner with the hidden attribute,
but the banner's display:flex overrode it, so the banner showed and started the
image-side tour, whose buttons are not on the audio page. The audio guide now
has its own spotlight steps, and tour steps whose target is not on screen are
skipped. The batch import screen shows a folded example of a folder layout that
sorts cleanly: one folder per character, named after it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 16:32:46 +08:00

121 lines
5.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { CURRENT_UPDATE_GUIDE, validateUpdateGuide } from './update_guide_data.js';
import { i18n, translate as t } from './locales.js';
import { createViewScope } from './ui_lifecycle.js';
import { text } from './ui_dom.js';
import { startSpotlightTour, ensureTourStyles } from './ui_spotlight_tour.js';
import { createVersionLine } from './ui_version_manager.js';
const acknowledgedThisSession = new Set();
let activeGuide = null;
const storageKey = id => `anomalous_update_guide_seen:${id}`;
export function hasAcknowledged(id = CURRENT_UPDATE_GUIDE.id) {
if (acknowledgedThisSession.has(id)) return true;
try { return localStorage.getItem(storageKey(id)) === '1'; }
catch (error) { return false; } // Storage may be unavailable; session state still works.
}
export function closeUpdateGuide(owner, acknowledge = false) {
if (!activeGuide || activeGuide.owner !== owner) return;
const current = activeGuide;
activeGuide = null;
if (acknowledge) {
acknowledgedThisSession.add(current.id);
try { localStorage.setItem(storageKey(current.id), '1'); }
catch (error) { console.warn('[AMB] Update guide dismissal is saved for this session only.', error); }
}
current.scope.dispose();
}
/** Opened only by explicit user action in the header or Help. No graph or file writes. */
export function showUpdateGuide(owner, { force = false, guide = CURRENT_UPDATE_GUIDE } = {}) {
if (!owner.modal?.classList.contains('visible')) return false;
if (!validateUpdateGuide(guide, i18n)) {
console.warn('[AMB] Invalid update guide configuration; guide skipped.');
return false;
}
if (activeGuide?.owner === owner && activeGuide.id === guide.id) return false;
if (!force && hasAcknowledged(guide.id)) return false;
if (activeGuide) closeUpdateGuide(activeGuide.owner);
ensureTourStyles();
const scope = createViewScope();
const previousFocus = document.activeElement;
const dialog = document.createElement('dialog');
dialog.className = 'anomalous-update-guide';
dialog.setAttribute('aria-labelledby', 'anomalous-update-guide-title');
dialog.setAttribute('aria-describedby', 'anomalous-update-guide-body');
activeGuide = { owner, id: guide.id, scope };
scope.onDispose(() => {
if (dialog.open) dialog.close();
dialog.remove();
if (previousFocus?.isConnected) previousFocus.focus({ preventScroll: true });
});
const header = text(dialog, 'div', '', 'anomalous-update-guide-header');
text(header, 'span', t(guide.titleKey || 'updateGuideTitle'));
const dismiss = text(header, 'button', '×', 'anomalous-update-guide-close');
dismiss.type = 'button';
dismiss.setAttribute('aria-label', t('close'));
dismiss.onclick = () => closeUpdateGuide(owner, true);
dialog.appendChild(createVersionLine(owner, { beforeOpen: () => closeUpdateGuide(owner, true) }));
const icon = text(dialog, 'div', '', 'anomalous-update-guide-icon');
icon.setAttribute('aria-hidden', 'true');
const title = text(dialog, 'h2', '');
title.id = 'anomalous-update-guide-title';
const body = text(dialog, 'p', '');
body.id = 'anomalous-update-guide-body';
const tourBanner = text(dialog, 'button', t('updateGuideTourBanner') || '💡 想在主界面实地体验?点击开启按键遮罩导览 ›', 'anomalous-update-guide-tour-banner');
tourBanner.type = 'button';
tourBanner.id = 'anomalous-update-guide-tour-btn';
tourBanner.hidden = guide.tour === false;
tourBanner.onclick = () => {
closeUpdateGuide(owner, true);
startSpotlightTour(owner, Array.isArray(guide.tour) ? { steps: guide.tour } : {});
};
const progress = text(dialog, 'p', '', 'anomalous-update-guide-progress');
progress.setAttribute('aria-live', 'polite');
const footer = text(dialog, 'div', '', 'anomalous-update-guide-footer');
const skip = text(footer, 'button', t('updateGuideSkip'), 'anomalous-btn-ghost');
const navGroup = text(footer, 'div', '', 'anomalous-update-guide-nav-group');
navGroup.style.display = 'flex';
navGroup.style.gap = '8px';
navGroup.style.alignItems = 'center';
const back = text(navGroup, 'button', t('updateGuideBack'), 'anomalous-btn-ghost');
const next = text(navGroup, 'button', '', 'anomalous-btn-primary');
for (const button of [skip, back, next, tourBanner]) button.type = 'button';
let index = 0;
const render = () => {
const step = guide.steps[index];
icon.textContent = step.icon;
title.textContent = t(step.titleKey);
body.textContent = t(step.bodyKey);
tourBanner.textContent = t('updateGuideTourBanner') || '💡 想在主界面实地体验?点击开启按键遮罩导览 ›';
progress.textContent = t('updateGuideProgress', { current: index + 1, total: guide.steps.length });
back.disabled = index === 0;
next.textContent = t(index === guide.steps.length - 1 ? 'updateGuideDone' : 'updateGuideNext');
};
skip.onclick = () => closeUpdateGuide(owner, true);
back.onclick = () => { if (index > 0) { index--; render(); } };
next.onclick = () => {
if (index === guide.steps.length - 1) closeUpdateGuide(owner, true);
else { index++; render(); }
};
scope.listen(dialog, 'keydown', event => {
if (event.key === 'Escape') {
event.preventDefault();
event.stopPropagation();
closeUpdateGuide(owner, true);
}
});
scope.listen(dialog, 'cancel', event => { event.preventDefault(); closeUpdateGuide(owner, true); });
scope.listen(dialog, 'close', () => closeUpdateGuide(owner));
scope.listen(dialog, 'click', event => { if (event.target === dialog) closeUpdateGuide(owner, true); });
render();
document.body.appendChild(dialog);
dialog.showModal();
next.focus();
return true;
}