sa2kit 1.4.3 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/dist/ConfigService-7MEZXKJ5.js +21 -0
  2. package/dist/ConfigService-7MEZXKJ5.js.map +1 -0
  3. package/dist/ConfigService-BV57YYFW.mjs +4 -0
  4. package/dist/ConfigService-BV57YYFW.mjs.map +1 -0
  5. package/dist/ConfigService-BxK06xP6.d.mts +262 -0
  6. package/dist/ConfigService-BxK06xP6.d.ts +262 -0
  7. package/dist/audioDetection/index.d.mts +449 -0
  8. package/dist/audioDetection/index.d.ts +449 -0
  9. package/dist/audioDetection/index.js +1244 -0
  10. package/dist/audioDetection/index.js.map +1 -0
  11. package/dist/audioDetection/index.mjs +1227 -0
  12. package/dist/audioDetection/index.mjs.map +1 -0
  13. package/dist/chunk-5XUE72Y3.mjs +1001 -0
  14. package/dist/chunk-5XUE72Y3.mjs.map +1 -0
  15. package/dist/chunk-DQVPZTVC.js +1009 -0
  16. package/dist/chunk-DQVPZTVC.js.map +1 -0
  17. package/dist/chunk-NEPD75MX.mjs +467 -0
  18. package/dist/chunk-NEPD75MX.mjs.map +1 -0
  19. package/dist/chunk-OEDY7GI4.js +473 -0
  20. package/dist/chunk-OEDY7GI4.js.map +1 -0
  21. package/dist/chunk-TFQF2HDO.mjs +354 -0
  22. package/dist/chunk-TFQF2HDO.mjs.map +1 -0
  23. package/dist/chunk-TOC5FSHP.js +358 -0
  24. package/dist/chunk-TOC5FSHP.js.map +1 -0
  25. package/dist/imageCrop/index.d.mts +165 -0
  26. package/dist/imageCrop/index.d.ts +165 -0
  27. package/dist/imageCrop/index.js +559 -0
  28. package/dist/imageCrop/index.js.map +1 -0
  29. package/dist/imageCrop/index.mjs +540 -0
  30. package/dist/imageCrop/index.mjs.map +1 -0
  31. package/dist/index.d.mts +139 -0
  32. package/dist/index.d.ts +139 -0
  33. package/dist/index.js +670 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/index.mjs +662 -0
  36. package/dist/index.mjs.map +1 -1
  37. package/dist/mmd/admin/index.d.mts +1 -1
  38. package/dist/mmd/admin/index.d.ts +1 -1
  39. package/dist/mmd/index.d.mts +138 -4
  40. package/dist/mmd/index.d.ts +138 -4
  41. package/dist/mmd/index.js +714 -12
  42. package/dist/mmd/index.js.map +1 -1
  43. package/dist/mmd/index.mjs +713 -15
  44. package/dist/mmd/index.mjs.map +1 -1
  45. package/dist/mmd/server/index.d.mts +1 -1
  46. package/dist/mmd/server/index.d.ts +1 -1
  47. package/dist/testYourself/admin/index.d.mts +58 -0
  48. package/dist/testYourself/admin/index.d.ts +58 -0
  49. package/dist/testYourself/admin/index.js +17 -0
  50. package/dist/testYourself/admin/index.js.map +1 -0
  51. package/dist/testYourself/admin/index.mjs +4 -0
  52. package/dist/testYourself/admin/index.mjs.map +1 -0
  53. package/dist/testYourself/index.d.mts +6 -98
  54. package/dist/testYourself/index.d.ts +6 -98
  55. package/dist/testYourself/index.js +90 -334
  56. package/dist/testYourself/index.js.map +1 -1
  57. package/dist/testYourself/index.mjs +47 -333
  58. package/dist/testYourself/index.mjs.map +1 -1
  59. package/dist/testYourself/server/index.d.mts +1029 -0
  60. package/dist/testYourself/server/index.d.ts +1029 -0
  61. package/dist/testYourself/server/index.js +42 -0
  62. package/dist/testYourself/server/index.js.map +1 -0
  63. package/dist/testYourself/server/index.mjs +5 -0
  64. package/dist/testYourself/server/index.mjs.map +1 -0
  65. package/dist/{types-Bc_p-zAR.d.mts → types-CsTSddwu.d.mts} +7 -0
  66. package/dist/{types-Bc_p-zAR.d.ts → types-CsTSddwu.d.ts} +7 -0
  67. package/dist/universalFile/server/index.js +5 -5
  68. package/dist/universalFile/server/index.mjs +1 -1
  69. package/package.json +62 -20
@@ -1,8 +1,8 @@
1
1
  import '../chunk-BJTO5JO5.mjs';
2
- import React6, { forwardRef, useRef, useImperativeHandle, useEffect, useState, useCallback } from 'react';
2
+ import React6, { forwardRef, useRef, useEffect, useImperativeHandle, useState, useCallback } from 'react';
3
3
  import * as THREE from 'three';
4
4
  import { OrbitControls, MMDLoader, MMDAnimationHelper } from 'three-stdlib';
5
- import { SkipBack, Pause, Play, SkipForward, Repeat, Repeat1, Grid3x3, Settings, Minimize, Maximize, X, Video, Check, User, Image, Music } from 'lucide-react';
5
+ import { SkipBack, Pause, Play, SkipForward, Camera, Repeat, Repeat1, Shuffle, ListMusic, Music, X, Grid3x3, Settings, Minimize, Maximize, Video, Check, User, Image } from 'lucide-react';
6
6
  import { createPortal } from 'react-dom';
7
7
 
8
8
  // src/mmd/utils/ammo-loader.ts
@@ -47,6 +47,9 @@ var loadAmmo = (path = "/libs/ammo.wasm.js") => {
47
47
  };
48
48
 
49
49
  // src/mmd/components/MMDPlayerBase.tsx
50
+ if (typeof window !== "undefined") {
51
+ THREE.Cache.enabled = true;
52
+ }
50
53
  async function waitForMaterialsReady(object, renderer, scene, camera) {
51
54
  const textures = [];
52
55
  let meshCount = 0;
@@ -160,6 +163,7 @@ var MMDPlayerBase = forwardRef((props, ref) => {
160
163
  onPause,
161
164
  onEnded,
162
165
  onTimeUpdate,
166
+ onCameraChange,
163
167
  className,
164
168
  style
165
169
  } = props;
@@ -180,6 +184,10 @@ var MMDPlayerBase = forwardRef((props, ref) => {
180
184
  const animationClipRef = useRef(null);
181
185
  const loopRef = useRef(loop);
182
186
  const audioRef = useRef(null);
187
+ const latestCallbacks = useRef({ onPlay, onPause, onEnded, onTimeUpdate });
188
+ useEffect(() => {
189
+ latestCallbacks.current = { onPlay, onPause, onEnded, onTimeUpdate };
190
+ }, [onPlay, onPause, onEnded, onTimeUpdate]);
183
191
  const physicsComponentsRef = useRef({
184
192
  configs: [],
185
193
  dispatchers: [],
@@ -194,18 +202,18 @@ var MMDPlayerBase = forwardRef((props, ref) => {
194
202
  if (!isReadyRef.current) return;
195
203
  isPlayingRef.current = true;
196
204
  if (!clockRef.current.running) clockRef.current.start();
197
- onPlay?.();
205
+ latestCallbacks.current.onPlay?.();
198
206
  },
199
207
  pause: () => {
200
208
  if (!isPlayingRef.current) return;
201
209
  isPlayingRef.current = false;
202
210
  clockRef.current.stop();
203
- onPause?.();
211
+ latestCallbacks.current.onPause?.();
204
212
  },
205
213
  stop: () => {
206
214
  isPlayingRef.current = false;
207
215
  clockRef.current.stop();
208
- onPause?.();
216
+ latestCallbacks.current.onPause?.();
209
217
  },
210
218
  seek: (time) => {
211
219
  console.warn("Seek not fully implemented in MMDPlayerBase yet");
@@ -226,6 +234,24 @@ var MMDPlayerBase = forwardRef((props, ref) => {
226
234
  rendererRef.current.render(sceneRef.current, cameraRef.current);
227
235
  }
228
236
  return rendererRef.current.domElement.toDataURL("image/png");
237
+ },
238
+ resetCamera: () => {
239
+ if (!cameraRef.current || !controlsRef.current) return;
240
+ const { cameraPosition, cameraTarget } = stage;
241
+ if (cameraPosition) {
242
+ const pos = cameraPosition;
243
+ cameraRef.current.position.set(pos.x, pos.y, pos.z);
244
+ } else {
245
+ cameraRef.current.position.set(0, 20, 30);
246
+ }
247
+ if (cameraTarget) {
248
+ const target = cameraTarget;
249
+ controlsRef.current.target.set(target.x, target.y, target.z);
250
+ } else {
251
+ controlsRef.current.target.set(0, 10, 0);
252
+ }
253
+ controlsRef.current.update();
254
+ onCameraChange?.(false);
229
255
  }
230
256
  }));
231
257
  useEffect(() => {
@@ -312,6 +338,11 @@ var MMDPlayerBase = forwardRef((props, ref) => {
312
338
  const scene = new THREE.Scene();
313
339
  if (stage.backgroundColor) {
314
340
  scene.background = new THREE.Color(stage.backgroundColor);
341
+ } else if (stage.backgroundImage) {
342
+ const textureLoader = new THREE.TextureLoader();
343
+ textureLoader.load(stage.backgroundImage, (texture) => {
344
+ scene.background = texture;
345
+ });
315
346
  }
316
347
  sceneRef.current = scene;
317
348
  const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 2e3);
@@ -368,6 +399,9 @@ var MMDPlayerBase = forwardRef((props, ref) => {
368
399
  controls.target.set(0, 10, 0);
369
400
  }
370
401
  controls.update();
402
+ controls.addEventListener("start", () => {
403
+ onCameraChange?.(true);
404
+ });
371
405
  controlsRef.current = controls;
372
406
  if (showAxes) {
373
407
  const axesHelper = new THREE.AxesHelper(20);
@@ -586,6 +620,25 @@ var MMDPlayerBase = forwardRef((props, ref) => {
586
620
  console.log("[MMDPlayerBase] \u2705 Stage materials and textures loaded");
587
621
  scene.add(stageMesh);
588
622
  console.log("[MMDPlayerBase] \u2705 Stage added to scene (fully loaded)");
623
+ if (resources.stageMotionPath && stageMesh) {
624
+ console.log("[MMDPlayerBase] Loading stage motion:", resources.stageMotionPath);
625
+ const anyLoader = loader;
626
+ const anyHelper = helper;
627
+ const anyStage = stageMesh;
628
+ anyLoader.loadAnimation(
629
+ resources.stageMotionPath,
630
+ anyStage,
631
+ (stageAnimation) => {
632
+ if (checkCancelled()) return;
633
+ anyHelper.add(anyStage, {
634
+ animation: stageAnimation
635
+ });
636
+ console.log("[MMDPlayerBase] \u2705 Stage motion bound successfully");
637
+ },
638
+ void 0,
639
+ (err) => console.error("Failed to load stage motion:", err)
640
+ );
641
+ }
589
642
  } catch (err) {
590
643
  console.error("Failed to load stage:", err);
591
644
  }
@@ -1031,6 +1084,38 @@ ${errorMessage}
1031
1084
  audioRef.current.setLoop(loop);
1032
1085
  }
1033
1086
  }, [loop]);
1087
+ useEffect(() => {
1088
+ if (!isReadyRef.current) return;
1089
+ if (sceneRef.current) {
1090
+ if (stage.backgroundColor) {
1091
+ sceneRef.current.background = new THREE.Color(stage.backgroundColor);
1092
+ } else if (stage.backgroundImage) {
1093
+ const textureLoader = new THREE.TextureLoader();
1094
+ textureLoader.load(stage.backgroundImage, (texture) => {
1095
+ if (sceneRef.current) sceneRef.current.background = texture;
1096
+ });
1097
+ }
1098
+ }
1099
+ if (sceneRef.current) {
1100
+ sceneRef.current.traverse((obj) => {
1101
+ if (obj instanceof THREE.AmbientLight && stage.ambientLightIntensity !== void 0) {
1102
+ obj.intensity = stage.ambientLightIntensity;
1103
+ }
1104
+ if (obj instanceof THREE.DirectionalLight && stage.directionalLightIntensity !== void 0) {
1105
+ obj.intensity = stage.directionalLightIntensity;
1106
+ }
1107
+ });
1108
+ }
1109
+ if (cameraRef.current && stage.cameraPosition) {
1110
+ const pos = stage.cameraPosition;
1111
+ cameraRef.current.position.set(pos.x, pos.y, pos.z);
1112
+ }
1113
+ if (controlsRef.current && stage.cameraTarget) {
1114
+ const target = stage.cameraTarget;
1115
+ controlsRef.current.target.set(target.x, target.y, target.z);
1116
+ controlsRef.current.update();
1117
+ }
1118
+ }, [stage.backgroundColor, stage.backgroundImage, stage.ambientLightIntensity, stage.directionalLightIntensity, stage.cameraPosition, stage.cameraTarget]);
1034
1119
  const animate = () => {
1035
1120
  animationIdRef.current = requestAnimationFrame(animate);
1036
1121
  if (rendererRef.current && sceneRef.current && cameraRef.current) {
@@ -1040,11 +1125,11 @@ ${errorMessage}
1040
1125
  const elapsed = clockRef.current.elapsedTime;
1041
1126
  const duration = durationRef.current;
1042
1127
  const currentTime = duration > 0 && loopRef.current ? elapsed % duration : elapsed;
1043
- onTimeUpdate?.(currentTime);
1128
+ latestCallbacks.current.onTimeUpdate?.(currentTime);
1044
1129
  if (!loopRef.current && duration > 0 && elapsed >= duration) {
1045
1130
  isPlayingRef.current = false;
1046
1131
  clockRef.current.stop();
1047
- onEnded?.();
1132
+ latestCallbacks.current.onEnded?.();
1048
1133
  }
1049
1134
  }
1050
1135
  rendererRef.current.render(sceneRef.current, cameraRef.current);
@@ -1073,6 +1158,7 @@ var ControlPanel = ({
1073
1158
  isFullscreen,
1074
1159
  isLooping,
1075
1160
  isListLooping,
1161
+ isCameraManual = false,
1076
1162
  showSettings,
1077
1163
  showAxes = false,
1078
1164
  showPrevNext = false,
@@ -1085,7 +1171,8 @@ var ControlPanel = ({
1085
1171
  onToggleAxes,
1086
1172
  onOpenSettings,
1087
1173
  onPrevious,
1088
- onNext
1174
+ onNext,
1175
+ onResetCamera
1089
1176
  }) => {
1090
1177
  return /* @__PURE__ */ React6.createElement("div", { className: "absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/80 to-transparent p-4 transition-opacity duration-300 hover:opacity-100" }, /* @__PURE__ */ React6.createElement("div", { className: "flex items-center justify-between text-white" }, /* @__PURE__ */ React6.createElement("div", { className: "flex items-center gap-2" }, showPrevNext && onPrevious && /* @__PURE__ */ React6.createElement(
1091
1178
  "button",
@@ -1127,6 +1214,14 @@ var ControlPanel = ({
1127
1214
  title: isLooping ? "\u5355\u66F2\u5FAA\u73AF\uFF1A\u5F00\u542F" : "\u5355\u66F2\u5FAA\u73AF\uFF1A\u5173\u95ED"
1128
1215
  },
1129
1216
  /* @__PURE__ */ React6.createElement(Repeat1, { size: 20 })
1217
+ ), isCameraManual && onResetCamera && /* @__PURE__ */ React6.createElement(
1218
+ "button",
1219
+ {
1220
+ onClick: onResetCamera,
1221
+ className: "rounded-full p-2 bg-blue-500/30 text-blue-400 hover:bg-blue-500/50 hover:text-blue-300 transition-all animate-in zoom-in duration-300",
1222
+ title: "\u6062\u590D\u521D\u59CB\u89C6\u89D2"
1223
+ },
1224
+ /* @__PURE__ */ React6.createElement(Camera, { size: 20 })
1130
1225
  ), onToggleAxes && /* @__PURE__ */ React6.createElement(
1131
1226
  "button",
1132
1227
  {
@@ -1533,6 +1628,7 @@ var MMDPlaylist = ({
1533
1628
  const [isListLooping, setIsListLooping] = useState(loop);
1534
1629
  const [showPlaylist, setShowPlaylist] = useState(false);
1535
1630
  const [isTransitioning, setIsTransitioning] = useState(false);
1631
+ const [isCameraManual, setIsCameraManual] = useState(false);
1536
1632
  const playerRef = useRef(null);
1537
1633
  const containerRef = useRef(null);
1538
1634
  const preloadedRef = useRef(/* @__PURE__ */ new Set());
@@ -1684,7 +1780,7 @@ var MMDPlaylist = ({
1684
1780
  key: currentNode.id,
1685
1781
  ref: playerRef,
1686
1782
  resources: currentNode.resources,
1687
- stage,
1783
+ stage: { ...stage, ...currentNode.stage },
1688
1784
  autoPlay: autoPlay && currentIndex === 0,
1689
1785
  loop: isLooping,
1690
1786
  showAxes,
@@ -1697,6 +1793,7 @@ var MMDPlaylist = ({
1697
1793
  },
1698
1794
  onPlay: () => setIsPlaying(true),
1699
1795
  onPause: () => setIsPlaying(false),
1796
+ onCameraChange: (isManual) => setIsCameraManual(isManual),
1700
1797
  onEnded: handleEnded,
1701
1798
  onError
1702
1799
  }
@@ -1712,6 +1809,7 @@ var MMDPlaylist = ({
1712
1809
  isFullscreen,
1713
1810
  isLooping,
1714
1811
  isListLooping,
1812
+ isCameraManual,
1715
1813
  showSettings: true,
1716
1814
  showAxes,
1717
1815
  showPrevNext,
@@ -1724,7 +1822,11 @@ var MMDPlaylist = ({
1724
1822
  onToggleLoop: () => setIsLooping(!isLooping),
1725
1823
  onToggleListLoop: () => setIsListLooping(!isListLooping),
1726
1824
  onToggleAxes: () => setShowAxes(!showAxes),
1727
- onOpenSettings: () => setShowPlaylist(!showPlaylist)
1825
+ onOpenSettings: () => setShowPlaylist(!showPlaylist),
1826
+ onResetCamera: () => {
1827
+ playerRef.current?.resetCamera();
1828
+ setIsCameraManual(false);
1829
+ }
1728
1830
  }
1729
1831
  )
1730
1832
  ), showPlaylist && /* @__PURE__ */ React6.createElement("div", { className: "absolute inset-0 z-20 flex items-end bg-black/80 backdrop-blur-sm" }, /* @__PURE__ */ React6.createElement("div", { className: "w-full max-h-[60vh] overflow-y-auto bg-gray-900/95 rounded-t-xl" }, /* @__PURE__ */ React6.createElement("div", { className: "sticky top-0 flex items-center justify-between bg-gray-800 px-4 py-3 border-b border-gray-700" }, /* @__PURE__ */ React6.createElement("div", null, /* @__PURE__ */ React6.createElement("h3", { className: "text-white font-semibold" }, playlist.name), /* @__PURE__ */ React6.createElement("p", { className: "text-xs text-gray-400 mt-0.5" }, "\u5171 ", nodes.length, " \u4E2A\u8282\u70B9")), /* @__PURE__ */ React6.createElement(
@@ -1826,6 +1928,8 @@ var DialogueBox = ({
1826
1928
  onToggleAuto,
1827
1929
  onOpenHistory,
1828
1930
  onSkip,
1931
+ onResetCamera,
1932
+ isCameraManual = false,
1829
1933
  showControls = true,
1830
1934
  showSkipButton = true,
1831
1935
  showAutoButton = true,
@@ -1989,6 +2093,21 @@ var DialogueBox = ({
1989
2093
  title: "\u5386\u53F2\u8BB0\u5F55"
1990
2094
  },
1991
2095
  "\u{1F4DC} \u5386\u53F2"
2096
+ ), isCameraManual && /* @__PURE__ */ React6.createElement(
2097
+ "button",
2098
+ {
2099
+ onClick: (e) => {
2100
+ e.stopPropagation();
2101
+ onResetCamera?.();
2102
+ },
2103
+ className: "px-4 py-2 text-xs rounded-xl text-white font-medium hover:text-white transition-all backdrop-blur-lg border border-blue-400/50 hover:border-blue-400 hover:scale-105 active:scale-95 shadow-lg animate-in zoom-in duration-300",
2104
+ style: {
2105
+ background: "linear-gradient(135deg, rgba(59, 130, 246, 0.3), rgba(37, 99, 235, 0.2))",
2106
+ boxShadow: "0 4px 16px rgba(59, 130, 246, 0.2), inset 0 1px 0 rgba(255, 255, 255, 0.2)"
2107
+ },
2108
+ title: "\u6062\u590D\u521D\u59CB\u89C6\u89D2"
2109
+ },
2110
+ "\u{1F3A5} \u6062\u590D\u89C6\u89D2"
1992
2111
  ), showAutoButton && /* @__PURE__ */ React6.createElement(
1993
2112
  "button",
1994
2113
  {
@@ -2572,6 +2691,105 @@ var LoadingOverlay = ({
2572
2691
  ));
2573
2692
  };
2574
2693
  LoadingOverlay.displayName = "LoadingOverlay";
2694
+ var SkipConfirmDialog = ({
2695
+ onConfirm,
2696
+ onCancel
2697
+ }) => {
2698
+ const [isMounted, setIsMounted] = React6.useState(false);
2699
+ React6.useEffect(() => {
2700
+ setIsMounted(true);
2701
+ }, []);
2702
+ if (!isMounted) return null;
2703
+ const content = /* @__PURE__ */ React6.createElement(
2704
+ "div",
2705
+ {
2706
+ className: "fixed inset-0 flex items-center justify-center bg-black/40 backdrop-blur-sm pointer-events-auto",
2707
+ style: { zIndex: 999999 }
2708
+ },
2709
+ /* @__PURE__ */ React6.createElement(
2710
+ "div",
2711
+ {
2712
+ className: "p-8 rounded-3xl border border-white/30 shadow-2xl max-w-sm w-full mx-4 overflow-hidden relative",
2713
+ style: {
2714
+ background: `linear-gradient(135deg,
2715
+ rgba(255, 255, 255, 0.2) 0%,
2716
+ rgba(255, 255, 255, 0.1) 100%)`,
2717
+ backdropFilter: "blur(32px) saturate(200%)",
2718
+ WebkitBackdropFilter: "blur(32px) saturate(200%)"
2719
+ }
2720
+ },
2721
+ /* @__PURE__ */ React6.createElement("h3", { className: "text-xl font-bold text-white mb-4 drop-shadow-md" }, "\u52A8\u753B\u5C1A\u672A\u64AD\u653E\u5B8C\u6210"),
2722
+ /* @__PURE__ */ React6.createElement("p", { className: "text-white/90 mb-8 leading-relaxed drop-shadow-sm" }, "\u5F53\u524D\u573A\u666F\u7684\u52A8\u753B\u8FD8\u6CA1\u6709\u5B8C\u6574\u64AD\u653E\u4E00\u904D\uFF0C\u662F\u5426\u76F4\u63A5\u8DF3\u8F6C\u5230\u4E0B\u4E00\u4E2A\u573A\u666F\uFF1F"),
2723
+ /* @__PURE__ */ React6.createElement("div", { className: "flex justify-end gap-4" }, /* @__PURE__ */ React6.createElement(
2724
+ "button",
2725
+ {
2726
+ onClick: onCancel,
2727
+ className: "px-6 py-2.5 rounded-2xl text-white/80 font-medium hover:text-white hover:bg-white/10 transition-all border border-white/10 hover:border-white/30"
2728
+ },
2729
+ "\u53D6\u6D88"
2730
+ ), /* @__PURE__ */ React6.createElement(
2731
+ "button",
2732
+ {
2733
+ onClick: onConfirm,
2734
+ className: "px-6 py-2.5 rounded-2xl bg-white/20 hover:bg-white/30 text-white font-bold transition-all border border-white/40 shadow-lg hover:scale-105 active:scale-95"
2735
+ },
2736
+ "\u76F4\u63A5\u8DF3\u8F6C"
2737
+ ))
2738
+ )
2739
+ );
2740
+ let portalContainer = document.getElementById("dialogue-portal-root");
2741
+ if (!portalContainer) {
2742
+ portalContainer = document.createElement("div");
2743
+ portalContainer.id = "dialogue-portal-root";
2744
+ portalContainer.style.cssText = "position: fixed; inset: 0; pointer-events: none; z-index: 999999;";
2745
+ document.body.appendChild(portalContainer);
2746
+ }
2747
+ return createPortal(content, portalContainer);
2748
+ };
2749
+ var ChoiceMenu = ({
2750
+ choices,
2751
+ onSelect,
2752
+ theme
2753
+ }) => {
2754
+ const [isMounted, setIsMounted] = React6.useState(false);
2755
+ React6.useEffect(() => {
2756
+ setIsMounted(true);
2757
+ }, []);
2758
+ if (!isMounted) return null;
2759
+ const content = /* @__PURE__ */ React6.createElement(
2760
+ "div",
2761
+ {
2762
+ className: "fixed inset-0 flex flex-col items-center justify-center bg-black/20 backdrop-blur-sm pointer-events-auto transition-all animate-in fade-in duration-500",
2763
+ style: { zIndex: 1e6 }
2764
+ },
2765
+ /* @__PURE__ */ React6.createElement("div", { className: "flex flex-col gap-4 w-full max-w-md px-6" }, choices.map((choice, index) => /* @__PURE__ */ React6.createElement(
2766
+ "button",
2767
+ {
2768
+ key: index,
2769
+ onClick: () => onSelect(choice),
2770
+ className: "w-full py-4 px-8 rounded-2xl text-white font-bold text-lg transition-all border border-white/20 shadow-xl hover:scale-105 active:scale-95 group relative overflow-hidden",
2771
+ style: {
2772
+ background: `linear-gradient(135deg,
2773
+ rgba(255, 255, 255, 0.2) 0%,
2774
+ rgba(255, 255, 255, 0.1) 100%)`,
2775
+ backdropFilter: "blur(32px) saturate(200%)",
2776
+ WebkitBackdropFilter: "blur(32px) saturate(200%)"
2777
+ }
2778
+ },
2779
+ /* @__PURE__ */ React6.createElement("div", { className: "absolute inset-0 bg-white/10 opacity-0 group-hover:opacity-100 transition-opacity" }),
2780
+ /* @__PURE__ */ React6.createElement("div", { className: "absolute inset-0 bg-gradient-to-r from-transparent via-white/5 to-transparent -translate-x-full group-hover:translate-x-full transition-transform duration-1000 ease-in-out" }),
2781
+ /* @__PURE__ */ React6.createElement("span", { className: "relative z-10 drop-shadow-md" }, choice.text)
2782
+ )))
2783
+ );
2784
+ let portalContainer = document.getElementById("dialogue-portal-root");
2785
+ if (!portalContainer) {
2786
+ portalContainer = document.createElement("div");
2787
+ portalContainer.id = "dialogue-portal-root";
2788
+ portalContainer.style.cssText = "position: fixed; inset: 0; pointer-events: none; z-index: 999999;";
2789
+ document.body.appendChild(portalContainer);
2790
+ }
2791
+ return createPortal(content, portalContainer);
2792
+ };
2575
2793
 
2576
2794
  // src/mmd/visual-novel/MMDVisualNovel.tsx
2577
2795
  var MMDVisualNovel = forwardRef(
@@ -2605,11 +2823,17 @@ var MMDVisualNovel = forwardRef(
2605
2823
  const [showHistory, setShowHistory] = useState(false);
2606
2824
  const [history, setHistory] = useState([]);
2607
2825
  const [isStarted, setIsStarted] = useState(autoStart);
2826
+ const [isVmdFinished, setIsVmdFinished] = useState(false);
2827
+ const [pendingNodeIndex, setPendingNodeIndex] = useState(null);
2828
+ const [showChoices, setShowChoices] = useState(false);
2829
+ const [isCameraManual, setIsCameraManual] = useState(false);
2608
2830
  const playerRef = useRef(null);
2609
2831
  const containerRef = useRef(null);
2610
2832
  const autoTimerRef = useRef(null);
2611
2833
  const typingCompleteRef = useRef(false);
2612
2834
  const isStartedRef = useRef(autoStart);
2835
+ const lastAnimationTimeRef = useRef(0);
2836
+ const isVmdFinishedRef = useRef(false);
2613
2837
  const currentNode = nodes[currentNodeIndex];
2614
2838
  const currentDialogue = currentNode?.dialogues[currentDialogueIndex] || null;
2615
2839
  const addToHistory = useCallback((dialogue, nodeIndex, dialogueIndex) => {
@@ -2637,6 +2861,8 @@ var MMDVisualNovel = forwardRef(
2637
2861
  addToHistory(nextDialogue, currentNodeIndex, nextDialogueIndex);
2638
2862
  onDialogueChange?.(nextDialogue, nextDialogueIndex, currentNodeIndex);
2639
2863
  typingCompleteRef.current = false;
2864
+ } else if (currentNode.choices && currentNode.choices.length > 0) {
2865
+ setShowChoices(true);
2640
2866
  } else {
2641
2867
  const nextNodeIndex = currentNodeIndex + 1;
2642
2868
  if (nextNodeIndex < nodes.length) {
@@ -2649,15 +2875,26 @@ var MMDVisualNovel = forwardRef(
2649
2875
  }
2650
2876
  }, [currentNode, currentDialogueIndex, currentNodeIndex, nodes.length, loop, addToHistory, onDialogueChange, onScriptComplete]);
2651
2877
  const goToNode = useCallback(
2652
- (nodeIndex) => {
2878
+ (nodeIndex, force = false) => {
2653
2879
  if (nodeIndex < 0 || nodeIndex >= nodes.length) return;
2654
2880
  if (isTransitioning) return;
2655
2881
  const node = nodes[nodeIndex];
2656
2882
  if (!node) return;
2883
+ const currentResources = nodes[currentNodeIndex]?.resources;
2884
+ if (!force && currentResources?.motionPath && !isVmdFinishedRef.current) {
2885
+ console.log("[MMDVisualNovel] VMD not finished, showing confirmation");
2886
+ setPendingNodeIndex(nodeIndex);
2887
+ return;
2888
+ }
2657
2889
  console.log(`[MMDVisualNovel] Transitioning to node ${nodeIndex}`);
2658
2890
  setIsTransitioning(true);
2659
2891
  setIsLoading(true);
2660
2892
  setIsAnimationPlaying(false);
2893
+ setIsVmdFinished(false);
2894
+ isVmdFinishedRef.current = false;
2895
+ setPendingNodeIndex(null);
2896
+ setShowChoices(false);
2897
+ lastAnimationTimeRef.current = 0;
2661
2898
  setTimeout(() => {
2662
2899
  setCurrentNodeIndex(nodeIndex);
2663
2900
  setCurrentDialogueIndex(0);
@@ -2675,7 +2912,7 @@ var MMDVisualNovel = forwardRef(
2675
2912
  }, 100);
2676
2913
  }, 300);
2677
2914
  },
2678
- [nodes, isTransitioning, addToHistory, onNodeChange, onDialogueChange]
2915
+ [nodes, isTransitioning, addToHistory, onNodeChange, onDialogueChange, currentNodeIndex, isVmdFinished]
2679
2916
  );
2680
2917
  const goToDialogue = useCallback(
2681
2918
  (dialogueIndex) => {
@@ -2724,6 +2961,10 @@ var MMDVisualNovel = forwardRef(
2724
2961
  setIsAutoMode((prev) => !prev);
2725
2962
  }, []);
2726
2963
  const handleSkip = useCallback(() => {
2964
+ if (currentNode?.choices && currentNode.choices.length > 0) {
2965
+ setShowChoices(true);
2966
+ return;
2967
+ }
2727
2968
  const nextNodeIndex = currentNodeIndex + 1;
2728
2969
  if (nextNodeIndex < nodes.length) {
2729
2970
  goToNode(nextNodeIndex);
@@ -2797,7 +3038,7 @@ var MMDVisualNovel = forwardRef(
2797
3038
  key: currentNode.id,
2798
3039
  ref: playerRef,
2799
3040
  resources: currentNode.resources,
2800
- stage,
3041
+ stage: { ...stage, ...currentNode.stage },
2801
3042
  autoPlay: isStarted,
2802
3043
  loop: currentNode.loopAnimation === true,
2803
3044
  mobileOptimization,
@@ -2815,6 +3056,27 @@ var MMDVisualNovel = forwardRef(
2815
3056
  console.log("[MMDVisualNovel] MMDPlayerBase onPlay called");
2816
3057
  setIsAnimationPlaying(true);
2817
3058
  },
3059
+ onTimeUpdate: (time) => {
3060
+ const duration = playerRef.current?.getDuration() || 0;
3061
+ const isNearEnd = duration > 0 && time > duration * 0.98;
3062
+ const isLooped = time < lastAnimationTimeRef.current && lastAnimationTimeRef.current > 0;
3063
+ if (isNearEnd || isLooped) {
3064
+ if (!isVmdFinishedRef.current) {
3065
+ console.log("[MMDVisualNovel] VMD finished/looped, marking as finished");
3066
+ isVmdFinishedRef.current = true;
3067
+ setIsVmdFinished(true);
3068
+ }
3069
+ }
3070
+ lastAnimationTimeRef.current = time;
3071
+ },
3072
+ onEnded: () => {
3073
+ console.log("[MMDVisualNovel] VMD ended, marking as finished");
3074
+ isVmdFinishedRef.current = true;
3075
+ setIsVmdFinished(true);
3076
+ },
3077
+ onCameraChange: (isManual) => {
3078
+ setIsCameraManual(isManual);
3079
+ },
2818
3080
  onError
2819
3081
  }
2820
3082
  )
@@ -2841,12 +3103,13 @@ var MMDVisualNovel = forwardRef(
2841
3103
  }
2842
3104
  ),
2843
3105
  (() => {
2844
- const shouldShow = isStarted && isAnimationPlaying && currentDialogue && !showHistory;
3106
+ const shouldShow = isStarted && isAnimationPlaying && currentDialogue && !showHistory && !showChoices;
2845
3107
  console.log("[MMDVisualNovel] DialogueBox render condition:", {
2846
3108
  isStarted,
2847
3109
  isAnimationPlaying,
2848
3110
  hasDialogue: !!currentDialogue,
2849
3111
  showHistory,
3112
+ showChoices,
2850
3113
  shouldShow,
2851
3114
  dialogue: currentDialogue
2852
3115
  });
@@ -2864,6 +3127,11 @@ var MMDVisualNovel = forwardRef(
2864
3127
  onToggleAuto: toggleAutoMode,
2865
3128
  onOpenHistory: () => setShowHistory(true),
2866
3129
  onSkip: handleSkip,
3130
+ onResetCamera: () => {
3131
+ playerRef.current?.resetCamera();
3132
+ setIsCameraManual(false);
3133
+ },
3134
+ isCameraManual,
2867
3135
  showControls: true,
2868
3136
  showSkipButton,
2869
3137
  showAutoButton,
@@ -2871,6 +3139,35 @@ var MMDVisualNovel = forwardRef(
2871
3139
  }
2872
3140
  ) : null;
2873
3141
  })(),
3142
+ pendingNodeIndex !== null && /* @__PURE__ */ React6.createElement(
3143
+ SkipConfirmDialog,
3144
+ {
3145
+ onConfirm: () => {
3146
+ if (pendingNodeIndex !== null) {
3147
+ goToNode(pendingNodeIndex, true);
3148
+ }
3149
+ },
3150
+ onCancel: () => {
3151
+ setPendingNodeIndex(null);
3152
+ }
3153
+ }
3154
+ ),
3155
+ showChoices && currentNode.choices && /* @__PURE__ */ React6.createElement(
3156
+ ChoiceMenu,
3157
+ {
3158
+ choices: currentNode.choices,
3159
+ theme: dialogueTheme,
3160
+ onSelect: (choice) => {
3161
+ choice.onSelect?.();
3162
+ if (choice.nextNodeIndex === currentNodeIndex) {
3163
+ goToDialogue(choice.nextDialogueIndex || 0);
3164
+ setShowChoices(false);
3165
+ } else {
3166
+ goToNode(choice.nextNodeIndex, true);
3167
+ }
3168
+ }
3169
+ }
3170
+ ),
2874
3171
  showHistory && /* @__PURE__ */ React6.createElement(
2875
3172
  HistoryPanel,
2876
3173
  {
@@ -2883,7 +3180,408 @@ var MMDVisualNovel = forwardRef(
2883
3180
  }
2884
3181
  );
2885
3182
  MMDVisualNovel.displayName = "MMDVisualNovel";
3183
+ var MusicControls = ({
3184
+ isPlaying,
3185
+ currentTime,
3186
+ duration,
3187
+ loopMode,
3188
+ isCameraManual = false,
3189
+ onPlayPause,
3190
+ onNext,
3191
+ onPrevious,
3192
+ onSeek,
3193
+ onToggleLoop,
3194
+ onTogglePlaylist,
3195
+ onResetCamera,
3196
+ className = ""
3197
+ }) => {
3198
+ const formatTime = (seconds) => {
3199
+ const mins = Math.floor(seconds / 60);
3200
+ const secs = Math.floor(seconds % 60);
3201
+ return `${mins}:${secs.toString().padStart(2, "0")}`;
3202
+ };
3203
+ const progress = duration > 0 ? currentTime / duration * 100 : 0;
3204
+ return /* @__PURE__ */ React6.createElement(
3205
+ "div",
3206
+ {
3207
+ className: `w-full max-w-4xl mx-auto px-6 py-4 bg-white/10 backdrop-blur-xl border border-white/20 rounded-2xl shadow-2xl pointer-events-auto transition-all group ${className}`
3208
+ },
3209
+ /* @__PURE__ */ React6.createElement("div", { className: "relative w-full h-1.5 bg-white/20 rounded-full mb-4 cursor-pointer group/progress overflow-hidden" }, /* @__PURE__ */ React6.createElement(
3210
+ "div",
3211
+ {
3212
+ className: "absolute h-full bg-blue-500 rounded-full transition-all duration-300",
3213
+ style: { width: `${progress}%` }
3214
+ }
3215
+ ), /* @__PURE__ */ React6.createElement(
3216
+ "input",
3217
+ {
3218
+ type: "range",
3219
+ min: 0,
3220
+ max: duration || 100,
3221
+ value: currentTime,
3222
+ onChange: (e) => onSeek(Number(e.target.value)),
3223
+ className: "absolute inset-0 w-full h-full opacity-0 cursor-pointer"
3224
+ }
3225
+ )),
3226
+ /* @__PURE__ */ React6.createElement("div", { className: "flex items-center justify-between" }, /* @__PURE__ */ React6.createElement("div", { className: "flex items-center gap-2 text-xs font-mono text-white/60 w-32" }, /* @__PURE__ */ React6.createElement("span", null, formatTime(currentTime)), /* @__PURE__ */ React6.createElement("span", null, "/"), /* @__PURE__ */ React6.createElement("span", null, formatTime(duration))), /* @__PURE__ */ React6.createElement("div", { className: "flex items-center gap-6" }, /* @__PURE__ */ React6.createElement(
3227
+ "button",
3228
+ {
3229
+ onClick: onPrevious,
3230
+ className: "text-white/80 hover:text-white transition-colors"
3231
+ },
3232
+ /* @__PURE__ */ React6.createElement(SkipBack, { className: "w-6 h-6 fill-current" })
3233
+ ), /* @__PURE__ */ React6.createElement(
3234
+ "button",
3235
+ {
3236
+ onClick: onPlayPause,
3237
+ className: "w-12 h-12 flex items-center justify-center bg-blue-500 hover:bg-blue-400 text-white rounded-full shadow-lg shadow-blue-500/20 transition-all hover:scale-105 active:scale-95"
3238
+ },
3239
+ isPlaying ? /* @__PURE__ */ React6.createElement(Pause, { className: "w-6 h-6 fill-current" }) : /* @__PURE__ */ React6.createElement(Play, { className: "w-6 h-6 fill-current ml-1" })
3240
+ ), /* @__PURE__ */ React6.createElement(
3241
+ "button",
3242
+ {
3243
+ onClick: onNext,
3244
+ className: "text-white/80 hover:text-white transition-colors"
3245
+ },
3246
+ /* @__PURE__ */ React6.createElement(SkipForward, { className: "w-6 h-6 fill-current" })
3247
+ )), /* @__PURE__ */ React6.createElement("div", { className: "flex items-center gap-4 w-32 justify-end" }, isCameraManual && /* @__PURE__ */ React6.createElement(
3248
+ "button",
3249
+ {
3250
+ onClick: onResetCamera,
3251
+ className: "text-blue-400 hover:text-blue-300 transition-colors animate-in zoom-in duration-300",
3252
+ title: "\u6062\u590D\u521D\u59CB\u89C6\u89D2"
3253
+ },
3254
+ /* @__PURE__ */ React6.createElement(Camera, { className: "w-5 h-5" })
3255
+ ), /* @__PURE__ */ React6.createElement(
3256
+ "button",
3257
+ {
3258
+ onClick: onToggleLoop,
3259
+ className: "text-white/60 hover:text-white transition-colors",
3260
+ title: loopMode
3261
+ },
3262
+ loopMode === "list" && /* @__PURE__ */ React6.createElement(Repeat, { className: "w-5 h-5" }),
3263
+ loopMode === "single" && /* @__PURE__ */ React6.createElement(Repeat1, { className: "w-5 h-5 text-blue-400" }),
3264
+ loopMode === "shuffle" && /* @__PURE__ */ React6.createElement(Shuffle, { className: "w-5 h-5 text-orange-400" })
3265
+ ), /* @__PURE__ */ React6.createElement(
3266
+ "button",
3267
+ {
3268
+ onClick: onTogglePlaylist,
3269
+ className: "text-white/60 hover:text-white transition-colors"
3270
+ },
3271
+ /* @__PURE__ */ React6.createElement(ListMusic, { className: "w-5 h-5" })
3272
+ )))
3273
+ );
3274
+ };
3275
+ var PlaylistPanel = ({
3276
+ tracks,
3277
+ currentIndex,
3278
+ isOpen,
3279
+ onClose,
3280
+ onSelectTrack,
3281
+ className = ""
3282
+ }) => {
3283
+ if (!isOpen) return null;
3284
+ return /* @__PURE__ */ React6.createElement(
3285
+ "div",
3286
+ {
3287
+ className: `fixed inset-y-0 right-0 w-80 bg-gray-900/90 backdrop-blur-2xl border-l border-white/10 shadow-2xl z-50 flex flex-col pointer-events-auto transform transition-transform duration-500 ease-out ${isOpen ? "translate-x-0" : "translate-x-full"} ${className}`
3288
+ },
3289
+ /* @__PURE__ */ React6.createElement("div", { className: "flex items-center justify-between p-6 border-b border-white/10" }, /* @__PURE__ */ React6.createElement("div", { className: "flex items-center gap-3" }, /* @__PURE__ */ React6.createElement(Music, { className: "w-5 h-5 text-blue-400" }), /* @__PURE__ */ React6.createElement("h3", { className: "text-lg font-bold text-white" }, "\u64AD\u653E\u5217\u8868")), /* @__PURE__ */ React6.createElement(
3290
+ "button",
3291
+ {
3292
+ onClick: onClose,
3293
+ className: "p-2 text-white/60 hover:text-white transition-colors"
3294
+ },
3295
+ /* @__PURE__ */ React6.createElement(X, { className: "w-5 h-5" })
3296
+ )),
3297
+ /* @__PURE__ */ React6.createElement("div", { className: "flex-1 overflow-y-auto p-4 space-y-2 custom-scrollbar" }, tracks.map((track, index) => {
3298
+ const isActive = index === currentIndex;
3299
+ return /* @__PURE__ */ React6.createElement(
3300
+ "button",
3301
+ {
3302
+ key: track.id,
3303
+ onClick: () => onSelectTrack(index),
3304
+ className: `w-full flex items-center gap-4 p-3 rounded-xl transition-all group ${isActive ? "bg-blue-500/20 border border-blue-500/30" : "hover:bg-white/5 border border-transparent"}`
3305
+ },
3306
+ /* @__PURE__ */ React6.createElement("div", { className: "relative w-12 h-12 flex-shrink-0 rounded-lg overflow-hidden bg-gray-800" }, track.coverUrl ? /* @__PURE__ */ React6.createElement("img", { src: track.coverUrl, alt: track.title, className: "w-full h-full object-cover" }) : /* @__PURE__ */ React6.createElement("div", { className: "w-full h-full flex items-center justify-center text-white/20" }, /* @__PURE__ */ React6.createElement(Music, { className: "w-6 h-6" })), isActive && /* @__PURE__ */ React6.createElement("div", { className: "absolute inset-0 bg-blue-500/40 flex items-center justify-center" }, /* @__PURE__ */ React6.createElement("div", { className: "flex gap-1 items-end h-4" }, /* @__PURE__ */ React6.createElement("div", { className: "w-1 bg-white animate-music-bar-1" }), /* @__PURE__ */ React6.createElement("div", { className: "w-1 bg-white animate-music-bar-2" }), /* @__PURE__ */ React6.createElement("div", { className: "w-1 bg-white animate-music-bar-3" })))),
3307
+ /* @__PURE__ */ React6.createElement("div", { className: "flex-1 text-left min-w-0" }, /* @__PURE__ */ React6.createElement("h4", { className: `text-sm font-bold truncate ${isActive ? "text-blue-400" : "text-white/90"}` }, track.title), /* @__PURE__ */ React6.createElement("p", { className: "text-xs text-white/40 truncate mt-0.5" }, track.artist || "\u672A\u77E5\u827A\u672F\u5BB6")),
3308
+ !isActive && /* @__PURE__ */ React6.createElement("div", { className: "opacity-0 group-hover:opacity-100 transition-opacity" }, /* @__PURE__ */ React6.createElement(Play, { className: "w-4 h-4 text-white/40 fill-current" }))
3309
+ );
3310
+ })),
3311
+ /* @__PURE__ */ React6.createElement("div", { className: "p-6 border-t border-white/10" }, /* @__PURE__ */ React6.createElement("p", { className: "text-xs text-gray-500 text-center" }, "\u5171 ", tracks.length, " \u9996\u66F2\u76EE")),
3312
+ /* @__PURE__ */ React6.createElement("style", { dangerouslySetInnerHTML: { __html: `
3313
+ .custom-scrollbar::-webkit-scrollbar {
3314
+ width: 4px;
3315
+ }
3316
+ .custom-scrollbar::-webkit-scrollbar-track {
3317
+ background: transparent;
3318
+ }
3319
+ .custom-scrollbar::-webkit-scrollbar-thumb {
3320
+ background: rgba(255, 255, 255, 0.1);
3321
+ border-radius: 10px;
3322
+ }
3323
+ .custom-scrollbar::-webkit-scrollbar-thumb:hover {
3324
+ background: rgba(255, 255, 255, 0.2);
3325
+ }
3326
+ @keyframes music-bar-1 {
3327
+ 0%, 100% { height: 4px; }
3328
+ 50% { height: 12px; }
3329
+ }
3330
+ @keyframes music-bar-2 {
3331
+ 0%, 100% { height: 8px; }
3332
+ 50% { height: 16px; }
3333
+ }
3334
+ @keyframes music-bar-3 {
3335
+ 0%, 100% { height: 6px; }
3336
+ 50% { height: 14px; }
3337
+ }
3338
+ .animate-music-bar-1 { animation: music-bar-1 0.6s infinite; }
3339
+ .animate-music-bar-2 { animation: music-bar-2 0.8s infinite; }
3340
+ .animate-music-bar-3 { animation: music-bar-3 0.7s infinite; }
3341
+ ` } })
3342
+ );
3343
+ };
3344
+ var TrackInfo = ({ track, className = "" }) => {
3345
+ return /* @__PURE__ */ React6.createElement("div", { className: `flex flex-col items-center text-center gap-2 ${className}` }, /* @__PURE__ */ React6.createElement("div", { className: "px-4 py-1.5 bg-black/40 backdrop-blur-md rounded-full border border-white/10 shadow-lg" }, /* @__PURE__ */ React6.createElement("h2", { className: "text-lg font-bold text-white tracking-wider truncate max-w-md" }, track.title)), /* @__PURE__ */ React6.createElement("p", { className: "text-sm font-medium text-white/60 drop-shadow-md" }, track.artist || "\u672A\u77E5\u827A\u672F\u5BB6"));
3346
+ };
3347
+
3348
+ // src/mmd/music-player/MMDMusicPlayer.tsx
3349
+ var MMDMusicPlayer = forwardRef(
3350
+ ({
3351
+ config,
3352
+ stage,
3353
+ mobileOptimization,
3354
+ initialTrackIndex = 0,
3355
+ onTrackChange,
3356
+ onPlayPause,
3357
+ onProgress,
3358
+ onError,
3359
+ className,
3360
+ style
3361
+ }, ref) => {
3362
+ const { tracks, autoPlay = false, defaultLoopMode = "list" } = config;
3363
+ const [currentIndex, setCurrentIndex] = useState(initialTrackIndex);
3364
+ const [isPlaying, setIsPlaying] = useState(autoPlay);
3365
+ const [isLoading, setIsLoading] = useState(true);
3366
+ const [isTransitioning, setIsTransitioning] = useState(false);
3367
+ const [currentTime, setCurrentTime] = useState(0);
3368
+ const [duration, setDuration] = useState(0);
3369
+ const [loopMode, setLoopMode] = useState(defaultLoopMode);
3370
+ const [showPlaylist, setShowPlaylist] = useState(false);
3371
+ const [isUIVisible, setIsUIVisible] = useState(true);
3372
+ const [isCameraManual, setIsCameraManual] = useState(false);
3373
+ const playerRef = useRef(null);
3374
+ const containerRef = useRef(null);
3375
+ const isStartedRef = useRef(autoPlay);
3376
+ const uiTimeoutRef = useRef(null);
3377
+ const currentTrack = tracks[currentIndex];
3378
+ const goToTrack = useCallback(
3379
+ (index) => {
3380
+ if (index < 0 || index >= tracks.length) return;
3381
+ if (isTransitioning) return;
3382
+ const track = tracks[index];
3383
+ if (!track) return;
3384
+ console.log(`[MMDMusicPlayer] Transitioning to track ${index}: ${track.title}`);
3385
+ setIsTransitioning(true);
3386
+ setIsLoading(true);
3387
+ const wasPlaying = isPlaying;
3388
+ setIsPlaying(false);
3389
+ setTimeout(() => {
3390
+ setCurrentIndex(index);
3391
+ setCurrentTime(0);
3392
+ setDuration(0);
3393
+ onTrackChange?.(track, index);
3394
+ setTimeout(() => {
3395
+ setIsTransitioning(false);
3396
+ if (wasPlaying) {
3397
+ isStartedRef.current = true;
3398
+ }
3399
+ }, 100);
3400
+ }, 300);
3401
+ },
3402
+ [tracks, isTransitioning, isPlaying, onTrackChange]
3403
+ );
3404
+ const next = useCallback(() => {
3405
+ let nextIndex = currentIndex + 1;
3406
+ if (loopMode === "shuffle") {
3407
+ nextIndex = Math.floor(Math.random() * tracks.length);
3408
+ } else if (nextIndex >= tracks.length) {
3409
+ nextIndex = 0;
3410
+ }
3411
+ goToTrack(nextIndex);
3412
+ }, [currentIndex, tracks.length, loopMode, goToTrack]);
3413
+ const previous = useCallback(() => {
3414
+ let prevIndex = currentIndex - 1;
3415
+ if (prevIndex < 0) {
3416
+ prevIndex = tracks.length - 1;
3417
+ }
3418
+ goToTrack(prevIndex);
3419
+ }, [currentIndex, tracks.length, goToTrack]);
3420
+ useImperativeHandle(
3421
+ ref,
3422
+ () => ({
3423
+ play: () => {
3424
+ setIsPlaying(true);
3425
+ isStartedRef.current = true;
3426
+ playerRef.current?.play();
3427
+ },
3428
+ pause: () => {
3429
+ setIsPlaying(false);
3430
+ isStartedRef.current = false;
3431
+ playerRef.current?.pause();
3432
+ },
3433
+ next,
3434
+ previous,
3435
+ goToTrack,
3436
+ setLoopMode,
3437
+ getState: () => ({
3438
+ currentIndex,
3439
+ isPlaying,
3440
+ currentTime,
3441
+ duration,
3442
+ loopMode
3443
+ })
3444
+ }),
3445
+ [next, previous, goToTrack, currentIndex, isPlaying, currentTime, duration, loopMode]
3446
+ );
3447
+ const handleEnded = useCallback(() => {
3448
+ if (loopMode === "single") {
3449
+ playerRef.current?.seek(0);
3450
+ playerRef.current?.play();
3451
+ } else {
3452
+ next();
3453
+ }
3454
+ }, [loopMode, next]);
3455
+ const handleTimeUpdate = useCallback((time) => {
3456
+ setCurrentTime(time);
3457
+ if (playerRef.current) {
3458
+ const total = playerRef.current.getDuration();
3459
+ setDuration(total);
3460
+ onProgress?.(time, total);
3461
+ }
3462
+ }, [onProgress]);
3463
+ const resetUITimeout = useCallback(() => {
3464
+ setIsUIVisible(true);
3465
+ if (uiTimeoutRef.current) clearTimeout(uiTimeoutRef.current);
3466
+ if (isPlaying) {
3467
+ uiTimeoutRef.current = setTimeout(() => {
3468
+ if (!showPlaylist) setIsUIVisible(false);
3469
+ }, 5e3);
3470
+ }
3471
+ }, [isPlaying, showPlaylist]);
3472
+ useEffect(() => {
3473
+ resetUITimeout();
3474
+ return () => {
3475
+ if (uiTimeoutRef.current) clearTimeout(uiTimeoutRef.current);
3476
+ };
3477
+ }, [resetUITimeout]);
3478
+ if (!currentTrack) {
3479
+ return /* @__PURE__ */ React6.createElement("div", { className: "flex h-full w-full items-center justify-center bg-black text-white" }, "\u64AD\u653E\u5217\u8868\u4E3A\u7A7A");
3480
+ }
3481
+ return /* @__PURE__ */ React6.createElement(
3482
+ "div",
3483
+ {
3484
+ ref: containerRef,
3485
+ className: `relative bg-black group ${className}`,
3486
+ style: { width: "100%", height: "100%", overflow: "hidden", ...style },
3487
+ onMouseMove: resetUITimeout,
3488
+ onClick: resetUITimeout
3489
+ },
3490
+ /* @__PURE__ */ React6.createElement(
3491
+ "div",
3492
+ {
3493
+ className: "absolute inset-0 w-full h-full",
3494
+ style: {
3495
+ zIndex: 0,
3496
+ opacity: isLoading || isTransitioning ? 0 : 1,
3497
+ transition: "opacity 0.5s ease-in-out"
3498
+ }
3499
+ },
3500
+ !isTransitioning && /* @__PURE__ */ React6.createElement(
3501
+ MMDPlayerBase,
3502
+ {
3503
+ key: currentTrack.id,
3504
+ ref: playerRef,
3505
+ resources: currentTrack.resources,
3506
+ stage: { ...stage, ...currentTrack.stage },
3507
+ autoPlay: isStartedRef.current,
3508
+ loop: loopMode === "single",
3509
+ mobileOptimization,
3510
+ onLoad: () => {
3511
+ console.log("[MMDMusicPlayer] Track loaded");
3512
+ setIsLoading(false);
3513
+ if (isStartedRef.current) {
3514
+ playerRef.current?.play();
3515
+ setIsPlaying(true);
3516
+ }
3517
+ },
3518
+ onPlay: () => {
3519
+ setIsPlaying(true);
3520
+ onPlayPause?.(true);
3521
+ },
3522
+ onPause: () => {
3523
+ setIsPlaying(false);
3524
+ onPlayPause?.(false);
3525
+ },
3526
+ onCameraChange: (isManual) => {
3527
+ setIsCameraManual(isManual);
3528
+ },
3529
+ onTimeUpdate: handleTimeUpdate,
3530
+ onEnded: handleEnded,
3531
+ onError
3532
+ }
3533
+ )
3534
+ ),
3535
+ (isLoading || isTransitioning) && /* @__PURE__ */ React6.createElement("div", { className: "absolute inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-md" }, /* @__PURE__ */ React6.createElement("div", { className: "flex flex-col items-center gap-4" }, /* @__PURE__ */ React6.createElement("div", { className: "h-12 w-12 animate-spin rounded-full border-4 border-white/20 border-t-blue-500" }), /* @__PURE__ */ React6.createElement("div", { className: "text-white font-medium" }, isTransitioning ? "\u51C6\u5907\u4E0B\u4E00\u9996..." : "\u6B63\u5728\u52A0\u8F7D\u821E\u53F0..."))),
3536
+ /* @__PURE__ */ React6.createElement(
3537
+ "div",
3538
+ {
3539
+ className: `absolute inset-0 z-10 flex flex-col justify-between transition-opacity duration-700 pointer-events-none ${isUIVisible ? "opacity-100" : "opacity-0"}`
3540
+ },
3541
+ /* @__PURE__ */ React6.createElement("div", { className: "pt-12 px-8 flex justify-center" }, /* @__PURE__ */ React6.createElement(TrackInfo, { track: currentTrack })),
3542
+ /* @__PURE__ */ React6.createElement("div", { className: "pb-12 px-8" }, /* @__PURE__ */ React6.createElement(
3543
+ MusicControls,
3544
+ {
3545
+ isPlaying,
3546
+ currentTime,
3547
+ duration,
3548
+ loopMode,
3549
+ isCameraManual,
3550
+ onPlayPause: () => isPlaying ? playerRef.current?.pause() : playerRef.current?.play(),
3551
+ onNext: next,
3552
+ onPrevious: previous,
3553
+ onSeek: (time) => playerRef.current?.seek(time),
3554
+ onResetCamera: () => {
3555
+ playerRef.current?.resetCamera();
3556
+ setIsCameraManual(false);
3557
+ },
3558
+ onToggleLoop: () => {
3559
+ const modes = ["list", "single", "shuffle"];
3560
+ const nextMode = modes[(modes.indexOf(loopMode) + 1) % modes.length];
3561
+ setLoopMode(nextMode);
3562
+ },
3563
+ onTogglePlaylist: () => setShowPlaylist(!showPlaylist)
3564
+ }
3565
+ ))
3566
+ ),
3567
+ /* @__PURE__ */ React6.createElement(
3568
+ PlaylistPanel,
3569
+ {
3570
+ tracks,
3571
+ currentIndex,
3572
+ isOpen: showPlaylist,
3573
+ onClose: () => setShowPlaylist(false),
3574
+ onSelectTrack: (index) => {
3575
+ goToTrack(index);
3576
+ setShowPlaylist(false);
3577
+ }
3578
+ }
3579
+ )
3580
+ );
3581
+ }
3582
+ );
3583
+ MMDMusicPlayer.displayName = "MMDMusicPlayer";
2886
3584
 
2887
- export { DialogueBox, HistoryPanel, LoadingOverlay, LoadingScreen, MMDPlayerBase, MMDPlayerEnhanced, MMDPlayerEnhancedDebugInfo, MMDPlaylist, MMDPlaylistDebugInfo, MMDVisualNovel, StartScreen, loadAmmo };
3585
+ export { DialogueBox, HistoryPanel, LoadingOverlay, LoadingScreen, MMDMusicPlayer, MMDPlayerBase, MMDPlayerEnhanced, MMDPlayerEnhancedDebugInfo, MMDPlaylist, MMDPlaylistDebugInfo, MMDVisualNovel, MusicControls, PlaylistPanel, StartScreen, TrackInfo, loadAmmo };
2888
3586
  //# sourceMappingURL=index.mjs.map
2889
3587
  //# sourceMappingURL=index.mjs.map