4.6.2#1002
Conversation
Backlog 界面点击语音按钮时只调用 play(),未暂停其他 正在播放的 backlog 音频元素,导致多条语音同时播放。 同时未暂停游戏内正在播放的 currentVocal 语音, 点击 backlog 语音时与游戏内语音混合。 通过 querySelectorAll 匹配所有 backlog audio 元素, 在播放新语音前暂停并重置其他元素;同时暂停并重置 currentVocal 元素以避免与 backlog 语音混合。 Closes #866
Backlog 打开时自动播放不应推进剧情,否则在历史界面 点选 backlog 语音暂停 currentVocal 后,auto 会立即推进 到下一句对话并触发新的 vocal 播放,导致 Backlog 中听到 两条声音。 - autoPlay 在 Backlog 打开时直接 return,避免推进 - Backlog 点击 backlog 语音时同时 unmount vocal-play perform,清理外部 pause 留下的状态不一致 属于 #866 修复的延伸场景。
移除 'audio.id !== currentAudioId' 排除判断。后续逻辑会 重置并播放当前音频(currentTime = 0 + play()),故前置 pause 不需要排除当前。简化代码,无功能差异。 反馈来源:PR review
- Keep all text after the first unescaped semicolon when parsing inline comments, instead of only taking the first split segment. - Add tests for normal inline comments and comment-only lines with multiple semicolons.
Treat empty setTransform content as an empty transform frame so
-writeDefault follows the same base default path as setTransform:{}.
Accept only object-shaped transform JSON for setTransform, changeFigure,
and changeBg. Non-object values from malformed or empty transform input
fall back to the default transform path instead of entering animation
frames.
fix: #987 next 后接场景跳转失效问题
…ueries feat: 增强编辑器预览 transform 查询协议与 overlay reference box 能力
Fix transform frame parsing for empty and non-object input
Prevent sync-scene from exposing reset-stage intermediate state while fast preview is settling. The preview runtime now suppresses stage snapshots during sync-scene, only publishes the settled view state, and ignores stale sync-scene runs before they can commit over the latest preview target.
fix: conditionally enable video skip on double click
fix: validate scene file extension
fix(parser): preserve semicolons in inline comments
…d-config fix(parser): 修正资源去重和构建配置
…tion fix: 修复 preview sync-scene 的中间 reset 状态暴露
…anup Fix/866 backlog audio cleanup
Deploying webgal-dev with
|
| Latest commit: |
6b4cbf6
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://6e4b44bd.webgal-dev.pages.dev |
There was a problem hiding this comment.
Code Review
This pull request introduces a flowchart feature to WebGAL, allowing players to track and jump to unlocked story nodes, and enhances the editor's live preview stability and protocol. It also refactors sentence advancement and fixes bugs in video playback, backlog audio, and comment parsing. The review feedback identifies several critical robustness issues, including potential runtime TypeErrors from unsafe property access on currentScene and node.data, unsafe string trimming in the transform parser, and an unconditional state commit when a forward step is blocked. Additionally, it suggests replacing the non-standard 'chinese' locale with 'zh-CN' for consistent time formatting.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| export function parseTransformFrame(raw: string): AnimationFrame | null { | ||
| const source = raw.trim(); | ||
| if (source === '') return null; | ||
|
|
||
| try { | ||
| const parsed: unknown = JSON.parse(source); | ||
| return isTransformFrame(parsed) ? parsed : null; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| export function parseSetTransformFrame(raw: string): AnimationFrame | null { | ||
| return parseTransformFrame(raw.trim() === '' ? '{}' : raw); | ||
| } |
There was a problem hiding this comment.
The functions parseTransformFrame and parseSetTransformFrame assume that the raw parameter is always a valid string. If raw is null, undefined, or of a non-string type, calling raw.trim() will throw a runtime TypeError. Adding a type guard to ensure raw is a string before trimming makes the parsing robust against unexpected inputs.
export function parseTransformFrame(raw: string | null | undefined): AnimationFrame | null {
if (typeof raw !== 'string') return null;
const source = raw.trim();
if (source === '') return null;
try {
const parsed: unknown = JSON.parse(source);
return isTransformFrame(parsed) ? parsed : null;
} catch {
return null;
}
}
export function parseSetTransformFrame(raw: string | null | undefined): AnimationFrame | null {
if (typeof raw !== 'string') return null;
return parseTransformFrame(raw.trim() === '' ? '{}' : raw);
}| forward(); | ||
| commitForward(); |
There was a problem hiding this comment.
In continueSentence, commitForward() is called unconditionally after forward(). However, forward() can return false if the forward step is blocked by a scene write lock (WebGAL.sceneManager.lockSceneWrite). If forward() fails, calling commitForward() will commit an invalid or stale state, potentially leading to state desynchronization. Only call commitForward() if forward() returns true.
if (forward()) {
commitForward();
}| public unlockCurrentScene(refreshSnapshot = false) { | ||
| if (!this.hasFlowchart()) return; | ||
| const sceneNames = new Set([ | ||
| normalizeSceneName(this.sceneManager.sceneData.currentScene.sceneName), | ||
| normalizeSceneName(this.sceneManager.sceneData.currentScene.sceneUrl), | ||
| ]); |
There was a problem hiding this comment.
In unlockCurrentScene, this.sceneManager.sceneData.currentScene is accessed directly. During initialization or scene transitions, sceneData or currentScene might be undefined or null, which would cause a runtime crash. Adding a defensive check prevents potential TypeErrors.
public unlockCurrentScene(refreshSnapshot = false) {
if (!this.hasFlowchart()) return;
const currentScene = this.sceneManager.sceneData?.currentScene;
if (!currentScene) return;
const sceneNames = new Set([
normalizeSceneName(currentScene.sceneName),
normalizeSceneName(currentScene.sceneUrl),
]);| private createSnapshot(): ISaveData { | ||
| return { | ||
| nowStageState: cloneDeep(stageStateManager.getViewStageState()), | ||
| backlog: [], | ||
| index: -1, | ||
| saveTime: new Date().toLocaleDateString() + ' ' + new Date().toLocaleTimeString('chinese', { hour12: false }), | ||
| sceneData: { | ||
| currentSentenceId: this.sceneManager.sceneData.currentSentenceId, | ||
| sceneStack: cloneDeep(this.sceneManager.sceneData.sceneStack), | ||
| sceneName: this.sceneManager.sceneData.currentScene.sceneName, | ||
| sceneUrl: this.sceneManager.sceneData.currentScene.sceneUrl, | ||
| }, | ||
| previewImage: '', | ||
| }; | ||
| } |
There was a problem hiding this comment.
In createSnapshot, this.sceneManager.sceneData.currentScene is accessed directly, which can throw a TypeError if it is null or undefined. Additionally, 'chinese' is not a standard BCP 47 language tag for toLocaleTimeString and may cause inconsistent formatting or errors across different browser environments. It is safer to use 'zh-CN' and add optional chaining/nullish coalescing fallback values.
| private createSnapshot(): ISaveData { | |
| return { | |
| nowStageState: cloneDeep(stageStateManager.getViewStageState()), | |
| backlog: [], | |
| index: -1, | |
| saveTime: new Date().toLocaleDateString() + ' ' + new Date().toLocaleTimeString('chinese', { hour12: false }), | |
| sceneData: { | |
| currentSentenceId: this.sceneManager.sceneData.currentSentenceId, | |
| sceneStack: cloneDeep(this.sceneManager.sceneData.sceneStack), | |
| sceneName: this.sceneManager.sceneData.currentScene.sceneName, | |
| sceneUrl: this.sceneManager.sceneData.currentScene.sceneUrl, | |
| }, | |
| previewImage: '', | |
| }; | |
| } | |
| private createSnapshot(): ISaveData { | |
| const currentScene = this.sceneManager.sceneData?.currentScene; | |
| return { | |
| nowStageState: cloneDeep(stageStateManager.getViewStageState()), | |
| backlog: [], | |
| index: -1, | |
| saveTime: new Date().toLocaleDateString('zh-CN') + ' ' + new Date().toLocaleTimeString('zh-CN', { hour12: false }), | |
| sceneData: { | |
| currentSentenceId: this.sceneManager.sceneData?.currentSentenceId ?? 0, | |
| sceneStack: cloneDeep(this.sceneManager.sceneData?.sceneStack ?? []), | |
| sceneName: currentScene?.sceneName ?? '', | |
| sceneUrl: currentScene?.sceneUrl ?? '', | |
| }, | |
| previewImage: '', | |
| }; | |
| } |
| private currentSceneKey() { | ||
| const { currentScene } = this.sceneManager.sceneData; | ||
| return `${normalizeSceneName(currentScene.sceneName)}|${normalizeSceneName(currentScene.sceneUrl)}`; | ||
| } |
There was a problem hiding this comment.
In currentSceneKey, destructuring currentScene from this.sceneManager.sceneData will throw a runtime error if sceneData or currentScene is null/undefined. Adding optional chaining and a fallback ensures safety.
private currentSceneKey() {
const currentScene = this.sceneManager.sceneData?.currentScene;
if (!currentScene) return '';
return normalizeSceneName(currentScene.sceneName) + '|' + normalizeSceneName(currentScene.sceneUrl);
}| function getNodeWidth(node: IFlowchartNode, flowchartId: string, lockedNodeVisibility: LockedNodeVisibility) { | ||
| if (lockedNodeVisibility === 'node' && !WebGAL.flowchartManager.isUnlocked(flowchartId, node.id)) | ||
| return NODE_MIN_WIDTH; | ||
| const label = node.data?.label || node.id; | ||
| const textWidth = Array.from(label).reduce((sum, char) => sum + (char.charCodeAt(0) > 255 ? 24 : 14), 0); | ||
| return Math.max(NODE_MIN_WIDTH, textWidth + 36); | ||
| } |
There was a problem hiding this comment.
In getNodeWidth, node.data?.label or node.id is passed directly to Array.from(). If either of these properties is not a string (e.g., a number or boolean from the JSON configuration), Array.from() will throw a runtime TypeError. Explicitly casting the label to a string first prevents this issue.
| function getNodeWidth(node: IFlowchartNode, flowchartId: string, lockedNodeVisibility: LockedNodeVisibility) { | |
| if (lockedNodeVisibility === 'node' && !WebGAL.flowchartManager.isUnlocked(flowchartId, node.id)) | |
| return NODE_MIN_WIDTH; | |
| const label = node.data?.label || node.id; | |
| const textWidth = Array.from(label).reduce((sum, char) => sum + (char.charCodeAt(0) > 255 ? 24 : 14), 0); | |
| return Math.max(NODE_MIN_WIDTH, textWidth + 36); | |
| } | |
| function getNodeWidth(node: IFlowchartNode, flowchartId: string, lockedNodeVisibility: LockedNodeVisibility) { | |
| if (lockedNodeVisibility === 'node' && !WebGAL.flowchartManager.isUnlocked(flowchartId, node.id)) | |
| return NODE_MIN_WIDTH; | |
| const label = String(node.data?.label ?? node.id); | |
| const textWidth = Array.from(label).reduce((sum, char) => sum + (char.charCodeAt(0) > 255 ? 24 : 14), 0); | |
| return Math.max(NODE_MIN_WIDTH, textWidth + 36); | |
| } |
No description provided.