diff --git a/scratch-gui/src/components/game-loading-overlay/game-loading-overlay.css b/scratch-gui/src/components/game-loading-overlay/game-loading-overlay.css
new file mode 100644
index 00000000..c0cbeeab
--- /dev/null
+++ b/scratch-gui/src/components/game-loading-overlay/game-loading-overlay.css
@@ -0,0 +1,49 @@
+.overlay {
+ position: absolute;
+ inset: 0;
+ z-index: 50;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 14px;
+ background: rgba(8, 8, 8, 0.92);
+ pointer-events: auto;
+ opacity: 1;
+ transition: opacity 0.28s ease-out, visibility 0.28s ease-out;
+}
+
+.overlay.hidden {
+ opacity: 0;
+ pointer-events: none;
+ visibility: hidden;
+}
+
+.text {
+ color: #f2f2f2;
+ font-size: 15px;
+ line-height: 1.5;
+ letter-spacing: 0.5px;
+}
+
+.barTrack {
+ width: min(72%, 280px);
+ height: 10px;
+ background: rgba(255, 255, 255, 0.16);
+ border-radius: 999px;
+ overflow: hidden;
+}
+
+.barFill {
+ height: 100%;
+ background: linear-gradient(90deg, #4fc3f7 0%, #29b6f6 100%);
+ border-radius: 999px;
+ transition: width 0.25s linear;
+}
+
+.percent {
+ color: #e0e0e0;
+ font-size: 18px;
+ font-weight: 600;
+ font-variant-numeric: tabular-nums;
+}
diff --git a/scratch-gui/src/components/game-loading-overlay/game-loading-overlay.jsx b/scratch-gui/src/components/game-loading-overlay/game-loading-overlay.jsx
new file mode 100644
index 00000000..ce319fda
--- /dev/null
+++ b/scratch-gui/src/components/game-loading-overlay/game-loading-overlay.jsx
@@ -0,0 +1,32 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import styles from './game-loading-overlay.css';
+
+const GameLoadingOverlay = ({ visible, progress }) => {
+ const pct = Math.max(0, Math.min(100, Math.round(Number(progress) || 0)));
+
+ return (
+
+ );
+};
+
+GameLoadingOverlay.propTypes = {
+ visible: PropTypes.bool,
+ progress: PropTypes.number
+};
+
+GameLoadingOverlay.defaultProps = {
+ visible: true,
+ progress: 0
+};
+
+export default GameLoadingOverlay;
diff --git a/scratch-gui/src/components/game-loading-overlay/use-game-load-progress.js b/scratch-gui/src/components/game-loading-overlay/use-game-load-progress.js
new file mode 100644
index 00000000..94b16b16
--- /dev/null
+++ b/scratch-gui/src/components/game-loading-overlay/use-game-load-progress.js
@@ -0,0 +1,47 @@
+import { useCallback, useEffect, useRef, useState } from 'react';
+
+const alreadyReady = () => typeof window !== 'undefined' && window.iscreate === 1;
+
+const useGameLoadProgress = () => {
+ const [progress, setProgress] = useState(() => (alreadyReady() ? 100 : 0));
+ const [visible, setVisible] = useState(() => !alreadyReady());
+ const hidingRef = useRef(false);
+
+ useEffect(() => {
+ if (!visible) return undefined;
+
+ const timer = setInterval(() => {
+ if (window.iscreate === 1) {
+ if (!hidingRef.current) {
+ hidingRef.current = true;
+ setProgress(100);
+ setTimeout(() => setVisible(false), 280);
+ }
+ clearInterval(timer);
+ return;
+ }
+ setProgress(prev => (prev >= 90 && prev < 99 ? prev + 1 : prev));
+ }, 300);
+
+ return () => clearInterval(timer);
+ }, [visible]);
+
+ const onUnityProgress = useCallback(unityProgress => {
+ const mapped = Math.round(Math.min(90, Number(unityProgress) * 90));
+ if (!Number.isFinite(mapped)) return;
+ setProgress(prev => (mapped > prev ? mapped : prev));
+ }, []);
+
+ const onUnityInstanceCreated = useCallback(() => {
+ setProgress(prev => Math.max(prev, 90));
+ }, []);
+
+ return {
+ progress,
+ visible,
+ onUnityProgress,
+ onUnityInstanceCreated
+ };
+};
+
+export default useGameLoadProgress;
diff --git a/scratch-gui/src/components/stage-cpp-unity/stage-unity.jsx b/scratch-gui/src/components/stage-cpp-unity/stage-unity.jsx
index 96d4900b..a10c9173 100644
--- a/scratch-gui/src/components/stage-cpp-unity/stage-unity.jsx
+++ b/scratch-gui/src/components/stage-cpp-unity/stage-unity.jsx
@@ -15,6 +15,8 @@ import disableDevtool from 'disable-devtool';
import { initDisableDevtool } from '../../playground/devtool-control.js';
import { getUnityPlayerConfig, loadUnityLoaderScript } from '../../lib/unity-build-loader.js';
import { debug } from 'scratch-vm/src/util/log.js';
+import GameLoadingOverlay from '../game-loading-overlay/game-loading-overlay.jsx';
+import useGameLoadProgress from '../game-loading-overlay/use-game-load-progress.js';
const MAX_LEVELS = 500; // 最大关卡数据数量
const STORAGE_KEY = 'player_levels_data'; // 使用命名空间,防止键名冲突
@@ -48,6 +50,16 @@ const UnityComponent = function (props) {
const [getBlockNum, setBlockNum] = useState(0);
const [testDal, setTestDal] = useState(-1);
const canvasRef = useRef(null);
+ const {
+ progress: gameLoadProgress,
+ visible: gameLoadVisible,
+ onUnityProgress,
+ onUnityInstanceCreated
+ } = useGameLoadProgress();
+ const onUnityProgressRef = useRef(onUnityProgress);
+ const onUnityInstanceCreatedRef = useRef(onUnityInstanceCreated);
+ onUnityProgressRef.current = onUnityProgress;
+ onUnityInstanceCreatedRef.current = onUnityInstanceCreated;
const [isGameOverVisible, setIsGameOverVisible] = useState(false);
const [isGameOkVisible, setIsGameOkVisible] = useState(false);
const [isLockNextButton, setIsLockNextButton] = useState(true);
@@ -1274,8 +1286,9 @@ const UnityComponent = function (props) {
window.iscreate = 0;
const boot = () => {
createUnityInstance(canvas, config, (progress) => {
- // console.log(`加载进度: ${progress * 100}%`);
+ onUnityProgressRef.current(progress);
}).then((unityInstance) => {
+ onUnityInstanceCreatedRef.current();
window.unityInstance = unityInstance;
window.waitForUnityStepFinish = waitForUnityStepFinish;
window.waitForUnityVehicleStepFinish = waitForUnityVehicleStepFinish;
@@ -1339,6 +1352,7 @@ const UnityComponent = function (props) {
}}
>
+
diff --git a/scratch-gui/src/components/stage-cpp/stage-cpp-clang.jsx b/scratch-gui/src/components/stage-cpp/stage-cpp-clang.jsx
index 8182c9db..9f87b906 100644
--- a/scratch-gui/src/components/stage-cpp/stage-cpp-clang.jsx
+++ b/scratch-gui/src/components/stage-cpp/stage-cpp-clang.jsx
@@ -1160,12 +1160,13 @@ const StageWrapperCppClang = () => {
))}
- {!isApiPanelReady && (
-
- )}
+
);
};
@@ -1186,6 +1187,12 @@ const StageWrapperCppClang = () => {
// 移除snippet标记,转换为普通文本
insertCode = code.replace(/#{([^}]*)}/g, '$1');
}
+ if (cursor > 0 && view.state.doc.sliceString(cursor - 1, cursor) !== '\n') {
+ insertCode = '\n' + insertCode;
+ }
+ if (!insertCode.endsWith('\n')) {
+ insertCode += '\n';
+ }
// 插入代码
view.dispatch({
@@ -1210,7 +1217,7 @@ const StageWrapperCppClang = () => {
-
+ {ApiDrawer()}
e.stopPropagation()}
diff --git a/scratch-gui/src/components/stage-cpp/stage-cpp.css b/scratch-gui/src/components/stage-cpp/stage-cpp.css
index 3359623d..65292699 100644
--- a/scratch-gui/src/components/stage-cpp/stage-cpp.css
+++ b/scratch-gui/src/components/stage-cpp/stage-cpp.css
@@ -531,6 +531,10 @@
cursor: not-allowed;
}
+.apiDrawerLoadingHidden {
+ display: none;
+}
+
.apiDrawerLoadingSpinner {
width: 38px;
height: 38px;
@@ -538,6 +542,7 @@
border-top-color: #ba68c8;
border-radius: 50%;
animation: apiDrawerLoadingSpin 0.9s linear infinite;
+ will-change: transform;
}
.apiDrawerLoadingText {
@@ -549,6 +554,9 @@
}
@keyframes apiDrawerLoadingSpin {
+ from {
+ transform: rotate(0deg);
+ }
to {
transform: rotate(360deg);
}
diff --git a/scratch-gui/src/components/stage-python-unity/stage-unity.jsx b/scratch-gui/src/components/stage-python-unity/stage-unity.jsx
index ab503671..eba69d70 100644
--- a/scratch-gui/src/components/stage-python-unity/stage-unity.jsx
+++ b/scratch-gui/src/components/stage-python-unity/stage-unity.jsx
@@ -15,6 +15,8 @@ import disableDevtool from 'disable-devtool';
import { initDisableDevtool } from '../../playground/devtool-control.js';
import { getUnityPlayerConfig, loadUnityLoaderScript } from '../../lib/unity-build-loader.js';
import { debug } from 'scratch-vm/src/util/log.js';
+import GameLoadingOverlay from '../game-loading-overlay/game-loading-overlay.jsx';
+import useGameLoadProgress from '../game-loading-overlay/use-game-load-progress.js';
const MAX_LEVELS = 500; // 最大关卡数据数量
const STORAGE_KEY = 'player_levels_data'; // 使用命名空间,防止键名冲突
@@ -48,6 +50,16 @@ const UnityComponent = function (props) {
const [getBlockNum, setBlockNum] = useState(0);
const [testDal, setTestDal] = useState(-1);
const canvasRef = useRef(null);
+ const {
+ progress: gameLoadProgress,
+ visible: gameLoadVisible,
+ onUnityProgress,
+ onUnityInstanceCreated
+ } = useGameLoadProgress();
+ const onUnityProgressRef = useRef(onUnityProgress);
+ const onUnityInstanceCreatedRef = useRef(onUnityInstanceCreated);
+ onUnityProgressRef.current = onUnityProgress;
+ onUnityInstanceCreatedRef.current = onUnityInstanceCreated;
const [isGameOverVisible, setIsGameOverVisible] = useState(false);
const [isGameOkVisible, setIsGameOkVisible] = useState(false);
const [isLockNextButton, setIsLockNextButton] = useState(true);
@@ -1259,8 +1271,9 @@ const UnityComponent = function (props) {
window.iscreate = 0;
const boot = () => {
createUnityInstance(canvas, config, (progress) => {
- // console.log(`加载进度: ${progress * 100}%`);
+ onUnityProgressRef.current(progress);
}).then((unityInstance) => {
+ onUnityInstanceCreatedRef.current();
window.unityInstance = unityInstance;
window.waitForUnityStepFinish = waitForUnityStepFinish;
window.waitForUnityVehicleStepFinish = waitForUnityVehicleStepFinish;
@@ -1324,6 +1337,7 @@ const UnityComponent = function (props) {
}}
>
+
diff --git a/scratch-gui/src/components/stage-python/stage-python.css b/scratch-gui/src/components/stage-python/stage-python.css
index 16a65291..a3d71bba 100644
--- a/scratch-gui/src/components/stage-python/stage-python.css
+++ b/scratch-gui/src/components/stage-python/stage-python.css
@@ -183,6 +183,10 @@
cursor: not-allowed;
}
+.apiDrawerLoadingHidden {
+ display: none;
+}
+
.apiDrawerLoadingSpinner {
width: 38px;
height: 38px;
@@ -190,6 +194,7 @@
border-top-color: #ba68c8;
border-radius: 50%;
animation: apiDrawerLoadingSpin 0.9s linear infinite;
+ will-change: transform;
}
.apiDrawerLoadingText {
@@ -201,6 +206,9 @@
}
@keyframes apiDrawerLoadingSpin {
+ from {
+ transform: rotate(0deg);
+ }
to {
transform: rotate(360deg);
}
diff --git a/scratch-gui/src/components/stage-python/stage-python.jsx b/scratch-gui/src/components/stage-python/stage-python.jsx
index dcb7fee4..c7fcfd76 100644
--- a/scratch-gui/src/components/stage-python/stage-python.jsx
+++ b/scratch-gui/src/components/stage-python/stage-python.jsx
@@ -1287,12 +1287,13 @@ sys.settrace(trace_func)`;
))}
- {!isApiPanelReady && (
-
- )}
+
);
};
@@ -1305,12 +1306,22 @@ sys.settrace(trace_func)`;
const { state } = view;
const { selection } = state;
const cursor = selection.main.head;
+ const withSafeNewlines = (text) => {
+ let next = text;
+ if (cursor > 0 && view.state.doc.sliceString(cursor - 1, cursor) !== '\n') {
+ next = '\n' + next;
+ }
+ if (!next.endsWith('\n')) {
+ next += '\n';
+ }
+ return next;
+ };
// 检查代码是否包含snippet占位符
if (code.includes('#{')) {
// 使用snippet功能插入代码
try {
- const snippetTemplate = snippet(code);
+ const snippetTemplate = snippet(withSafeNewlines(code));
const completion = snippetTemplate(view, {
from: cursor,
to: cursor
@@ -1320,7 +1331,7 @@ sys.settrace(trace_func)`;
view.dispatch(completion);
} else {
// 如果snippet创建失败,使用简化的插入方式
- const cleanCode = code.replace(/#{([^}]*)}/g, '$1');
+ const cleanCode = withSafeNewlines(code.replace(/#{([^}]*)}/g, '$1'));
view.dispatch({
changes: {
from: cursor,
@@ -1332,7 +1343,7 @@ sys.settrace(trace_func)`;
} catch (error) {
console.warn('Snippet insertion failed, falling back to plain text:', error);
// 回退到普通插入,移除snippet标记
- const cleanCode = code.replace(/#{([^}]*)}/g, '$1');
+ const cleanCode = withSafeNewlines(code.replace(/#{([^}]*)}/g, '$1'));
view.dispatch({
changes: {
from: cursor,
@@ -1342,15 +1353,15 @@ sys.settrace(trace_func)`;
});
}
} else {
- // 普通代码插入
+ const insertText = withSafeNewlines(code);
view.dispatch({
changes: {
from: cursor,
to: cursor,
- insert: code
+ insert: insertText
},
selection: {
- anchor: cursor + code.length
+ anchor: cursor + insertText.length
}
});
}
@@ -1365,7 +1376,7 @@ sys.settrace(trace_func)`;
<>
-
+ {ApiDrawer()}
e.stopPropagation()}
diff --git a/scratch-gui/src/components/stage-unity/stage-unity.jsx b/scratch-gui/src/components/stage-unity/stage-unity.jsx
index 06f4825d..3059780f 100644
--- a/scratch-gui/src/components/stage-unity/stage-unity.jsx
+++ b/scratch-gui/src/components/stage-unity/stage-unity.jsx
@@ -15,6 +15,8 @@ import disableDevtool from 'disable-devtool';
import { initDisableDevtool } from '../../playground/devtool-control.js';
import { getUnityPlayerConfig, loadUnityLoaderScript } from '../../lib/unity-build-loader.js';
import { debug } from 'scratch-vm/src/util/log.js';
+import GameLoadingOverlay from '../game-loading-overlay/game-loading-overlay.jsx';
+import useGameLoadProgress from '../game-loading-overlay/use-game-load-progress.js';
const MAX_LEVELS = 500; // 最大关卡数据数量
const STORAGE_KEY = 'player_levels_data'; // 使用命名空间,防止键名冲突
@@ -248,6 +250,16 @@ const UnityComponent = function (props) {
const [getBlockNum, setBlockNum] = useState(0);
const [testDal, setTestDal] = useState(-1);
const canvasRef = useRef(null);
+ const {
+ progress: gameLoadProgress,
+ visible: gameLoadVisible,
+ onUnityProgress,
+ onUnityInstanceCreated
+ } = useGameLoadProgress();
+ const onUnityProgressRef = useRef(onUnityProgress);
+ const onUnityInstanceCreatedRef = useRef(onUnityInstanceCreated);
+ onUnityProgressRef.current = onUnityProgress;
+ onUnityInstanceCreatedRef.current = onUnityInstanceCreated;
const [isGameOverVisible, setIsGameOverVisible] = useState(false);
const [isGameOkVisible, setIsGameOkVisible] = useState(false);
const [isLockNextButton, setIsLockNextButton] = useState(true);
@@ -1522,8 +1534,9 @@ const UnityComponent = function (props) {
window.iscreate = 0;
const boot = () => {
createUnityInstance(canvas, config, (progress) => {
- // console.log(`加载进度: ${progress * 100}%`);
+ onUnityProgressRef.current(progress);
}).then((unityInstance) => {
+ onUnityInstanceCreatedRef.current();
window.unityInstance = unityInstance;
window.waitForUnityStepFinish = waitForUnityStepFinish;
window.waitForUnityVehicleStepFinish = waitForUnityVehicleStepFinish;
@@ -1587,6 +1600,7 @@ const UnityComponent = function (props) {
}}
>
+