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 ( +
+
正在加载中
+
+
+
+
{pct}%
+
+ ); +}; + +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) { }} > +