seq-ani-player.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913
  1. // PNG 序列播放工具 - 主逻辑
  2. // 负责动画播放、帧管理等功能
  3. (function () {
  4. // 调试日志开关
  5. const DEBUG = true;
  6. function log(...args) {
  7. if (DEBUG) console.log("[SeqAniPlayer]", ...args);
  8. }
  9. function logError(...args) {
  10. console.error("[SeqAniPlayer ERROR]", ...args);
  11. }
  12. function logWarn(...args) {
  13. console.warn("[SeqAniPlayer WARN]", ...args);
  14. }
  15. const foldersApi = "http://localhost:3000/api/folders";
  16. // 获取当前登录用户名
  17. let currentUsername = null;
  18. function getCurrentUsername() {
  19. if (currentUsername) {
  20. return currentUsername;
  21. }
  22. try {
  23. const loginDataStr = localStorage.getItem('loginData');
  24. if (!loginDataStr) {
  25. return null;
  26. }
  27. const loginData = JSON.parse(loginDataStr);
  28. const now = Date.now();
  29. // 检查是否过期
  30. if (now >= loginData.expireTime) {
  31. localStorage.removeItem('loginData');
  32. return null;
  33. }
  34. currentUsername = loginData.user ? loginData.user.username : null;
  35. return currentUsername;
  36. } catch (error) {
  37. console.error('[SeqAniPlayer] 获取用户名失败:', error);
  38. return null;
  39. }
  40. }
  41. const fpsSlider = document.getElementById("fpsSlider");
  42. const fpsValue = document.getElementById("fpsValue");
  43. const cardsGrid = document.getElementById("cardsGrid");
  44. const cardCountLabel = document.getElementById("cardCount");
  45. const folderNameLabel = document.getElementById("folderName");
  46. const folderCounterLabel = document.getElementById("folderCounter");
  47. const frameCounterLabel = document.getElementById("frameCounter");
  48. const downloadBtn = document.getElementById("downloadBtn");
  49. const prevBtn = document.getElementById("prevFolderBtn");
  50. const nextBtn = document.getElementById("nextFolderBtn");
  51. const playerImage = document.getElementById("playerImage");
  52. const playerLoadingOverlay = document.getElementById("playerLoadingOverlay");
  53. const playerError = document.getElementById("playerError");
  54. const playerShell = document.querySelector(".player-image-shell");
  55. const dropHint = document.getElementById("dropHint");
  56. let availableFolders = [];
  57. let cards = [];
  58. let cardTemplate = null;
  59. let cardTemplatePromise = null;
  60. let currentIndex = -1;
  61. let currentFps = 8;
  62. let currentFolder = "";
  63. let frameList = [];
  64. let currentFrameCursor = 0;
  65. let stageTimer = null;
  66. let frameSourceMode = "remote";
  67. let localFrameResources = [];
  68. function padFrame(index) {
  69. return index.toString().padStart(2, "0");
  70. }
  71. function buildFrameSrc(folder, index) {
  72. const frameName = padFrame(index);
  73. const username = getCurrentUsername();
  74. if (!username) {
  75. logWarn('未登录,无法加载图片');
  76. return '';
  77. }
  78. // 使用API路径,从用户目录加载
  79. const imagePath = `${folder}/${frameName}.png`;
  80. return `/api/disk/preview?username=${encodeURIComponent(username)}&path=${encodeURIComponent(imagePath)}`;
  81. }
  82. function buildFolderName(prefix, index, padding) {
  83. const num = index.toString().padStart(padding, "0");
  84. return `${prefix}${num}`;
  85. }
  86. async function ensureCardTemplate() {
  87. if (cardTemplate) {
  88. return cardTemplate;
  89. }
  90. if (cardTemplatePromise) {
  91. return cardTemplatePromise;
  92. }
  93. cardTemplatePromise = fetch("./card.html")
  94. .then((response) => {
  95. if (!response.ok) {
  96. throw new Error("Failed to load card template");
  97. }
  98. return response.text();
  99. })
  100. .then((html) => {
  101. const wrapper = document.createElement("div");
  102. wrapper.innerHTML = html.trim();
  103. const template = wrapper.querySelector("#card-template");
  104. if (!template) {
  105. throw new Error("Card template missing");
  106. }
  107. cardTemplate = template;
  108. return cardTemplate;
  109. })
  110. .catch((error) => {
  111. console.error(error);
  112. cardTemplatePromise = null;
  113. throw error;
  114. });
  115. return cardTemplatePromise;
  116. }
  117. function destroyCards() {
  118. cards.forEach((card) => card.destroy && card.destroy());
  119. cards = [];
  120. if (cardsGrid) {
  121. cardsGrid.innerHTML = "";
  122. }
  123. }
  124. function updateCardCount() {
  125. if (!cardCountLabel) return;
  126. cardCountLabel.textContent = `${availableFolders.length} 个动画`;
  127. }
  128. function updateNavDisabledState() {
  129. const disabled = availableFolders.length <= 1;
  130. if (prevBtn) prevBtn.disabled = disabled;
  131. if (nextBtn) nextBtn.disabled = disabled;
  132. }
  133. function updateMeta() {
  134. const total = availableFolders.length;
  135. const safeIndex = currentIndex >= 0 ? currentIndex : 0;
  136. const folderName = availableFolders[safeIndex] || "--";
  137. if (folderNameLabel) {
  138. folderNameLabel.textContent = folderName;
  139. }
  140. if (folderCounterLabel) {
  141. folderCounterLabel.textContent = total ? `${safeIndex + 1} / ${total}` : "0 / 0";
  142. }
  143. }
  144. function showLoading(isLoading) {
  145. if (!playerLoadingOverlay) return;
  146. playerLoadingOverlay.classList.toggle("is-visible", isLoading);
  147. }
  148. function showPlayerError(message) {
  149. if (!playerError) return;
  150. playerError.textContent = message;
  151. playerError.hidden = false;
  152. }
  153. function hidePlayerError() {
  154. if (!playerError) return;
  155. playerError.hidden = true;
  156. }
  157. function stopStageAnimation() {
  158. if (stageTimer) {
  159. clearInterval(stageTimer);
  160. stageTimer = null;
  161. }
  162. }
  163. function setStagePlaceholderVisible(isVisible) {
  164. if (!playerImage) {
  165. return;
  166. }
  167. playerImage.classList.toggle("is-hidden", Boolean(isVisible));
  168. if (isVisible) {
  169. playerImage.removeAttribute("src");
  170. }
  171. }
  172. function cleanupLocalFrames() {
  173. if (!localFrameResources.length) {
  174. return;
  175. }
  176. localFrameResources.forEach((frame) => {
  177. if (frame && frame.url) {
  178. URL.revokeObjectURL(frame.url);
  179. }
  180. });
  181. localFrameResources = [];
  182. }
  183. function switchToRemoteSource() {
  184. if (frameSourceMode !== "remote") {
  185. cleanupLocalFrames();
  186. frameSourceMode = "remote";
  187. }
  188. }
  189. function startStageLoop() {
  190. log("🎬 启动动画循环");
  191. stopStageAnimation();
  192. if (!frameList.length) {
  193. logWarn("帧列表为空,无法启动循环");
  194. return;
  195. }
  196. const interval = 1000 / currentFps;
  197. log(`⏱️ 动画循环启动,帧数: ${frameList.length}, FPS: ${currentFps}, 间隔: ${interval}ms`);
  198. stageTimer = setInterval(() => {
  199. if (!frameList.length) {
  200. stopStageAnimation();
  201. return;
  202. }
  203. currentFrameCursor = (currentFrameCursor + 1) % frameList.length;
  204. updateStageImage(frameList[currentFrameCursor]);
  205. }, interval);
  206. }
  207. function updateStageImage(frameData) {
  208. if (!playerImage) {
  209. logWarn("playerImage 元素不存在");
  210. return;
  211. }
  212. if (frameSourceMode === "local") {
  213. const frame = typeof frameData === "object" ? frameData : frameList[frameData];
  214. if (!frame || !frame.url) {
  215. logWarn("本地模式:帧数据无效");
  216. return;
  217. }
  218. setStagePlaceholderVisible(false);
  219. if (playerImage.src !== frame.url) {
  220. log(`🖼️ 更新图片: ${frame.name}`);
  221. playerImage.src = frame.url;
  222. }
  223. return;
  224. }
  225. if (!currentFolder) {
  226. logWarn("远程模式:文件夹未设置");
  227. return;
  228. }
  229. const frameNumber = typeof frameData === "number" ? frameData : parseInt(frameData, 10);
  230. if (Number.isNaN(frameNumber)) {
  231. logWarn("远程模式:帧编号无效");
  232. return;
  233. }
  234. const nextSrc = buildFrameSrc(currentFolder, frameNumber);
  235. setStagePlaceholderVisible(false);
  236. if (playerImage.src !== nextSrc) {
  237. playerImage.src = nextSrc;
  238. }
  239. }
  240. function setFps(fps) {
  241. currentFps = fps;
  242. if (frameList.length > 0) {
  243. startStageLoop();
  244. }
  245. }
  246. async function buildCards() {
  247. if (!cardsGrid || !window.SequenceCard) {
  248. return;
  249. }
  250. const template = await ensureCardTemplate();
  251. destroyCards();
  252. availableFolders.forEach((folderName, index) => {
  253. const templateContent = template.content.firstElementChild;
  254. if (!templateContent) {
  255. return;
  256. }
  257. const cardElement = templateContent.cloneNode(true);
  258. cardsGrid.appendChild(cardElement);
  259. const cardInstance = new window.SequenceCard(
  260. cardElement,
  261. folderName,
  262. index,
  263. buildFrameSrc,
  264. handleCardSelect
  265. );
  266. cardInstance.loadPreview();
  267. cards.push(cardInstance);
  268. });
  269. updateCardCount();
  270. }
  271. function handleCardSelect(folderName, cardIndex) {
  272. if (typeof cardIndex !== "number") {
  273. return;
  274. }
  275. selectCardByIndex(cardIndex);
  276. }
  277. function highlightActiveCard() {
  278. cards.forEach((card) => {
  279. if (typeof card.setActive === "function") {
  280. card.setActive(card.index === currentIndex);
  281. }
  282. });
  283. }
  284. function resetStage() {
  285. stopStageAnimation();
  286. switchToRemoteSource();
  287. frameList = [];
  288. currentFolder = "";
  289. currentFrameCursor = 0;
  290. showLoading(false);
  291. hidePlayerError();
  292. setStagePlaceholderVisible(true);
  293. if (frameCounterLabel) {
  294. frameCounterLabel.textContent = "0 帧";
  295. }
  296. if (folderNameLabel) {
  297. folderNameLabel.textContent = "--";
  298. }
  299. if (folderCounterLabel) {
  300. folderCounterLabel.textContent = "0 / 0";
  301. }
  302. }
  303. async function selectCardByIndex(index) {
  304. if (!availableFolders.length) {
  305. resetStage();
  306. return;
  307. }
  308. const total = availableFolders.length;
  309. currentIndex = ((index % total) + total) % total;
  310. currentFolder = availableFolders[currentIndex];
  311. updateMeta();
  312. highlightActiveCard();
  313. await startStageForFolder(currentFolder);
  314. }
  315. function sanitizeFrameList(frameInfo) {
  316. if (frameInfo && Array.isArray(frameInfo.frames) && frameInfo.frames.length > 0) {
  317. return frameInfo.frames;
  318. }
  319. const maxFrame = frameInfo && frameInfo.maxFrame ? frameInfo.maxFrame : 0;
  320. if (!maxFrame) {
  321. return [];
  322. }
  323. return Array.from({ length: maxFrame }, (_, idx) => idx + 1);
  324. }
  325. async function startStageForFolder(folderName) {
  326. stopStageAnimation();
  327. switchToRemoteSource();
  328. frameList = [];
  329. currentFrameCursor = 0;
  330. showLoading(true);
  331. hidePlayerError();
  332. if (frameCounterLabel) {
  333. frameCounterLabel.textContent = "0 帧";
  334. }
  335. if (!folderName) {
  336. showLoading(false);
  337. showPlayerError("未选择动画");
  338. return;
  339. }
  340. try {
  341. const response = await fetch(`http://localhost:3000/api/frames/${folderName}`);
  342. if (!response.ok) {
  343. throw new Error("Failed to fetch frames");
  344. }
  345. const data = await response.json();
  346. frameList = sanitizeFrameList(data);
  347. if (!frameList.length) {
  348. showPlayerError("暂无可用帧");
  349. setStagePlaceholderVisible(true);
  350. showLoading(false);
  351. return;
  352. }
  353. if (frameCounterLabel) {
  354. frameCounterLabel.textContent = `${frameList.length} 帧`;
  355. }
  356. updateStageImage(frameList[0]);
  357. showLoading(false);
  358. startStageLoop();
  359. } catch (error) {
  360. console.error(error);
  361. showLoading(false);
  362. showPlayerError("加载失败");
  363. }
  364. }
  365. async function loadAvailableFolders() {
  366. availableFolders = [];
  367. try {
  368. const response = await fetch(foldersApi);
  369. if (!response.ok) {
  370. throw new Error("Failed to fetch folders");
  371. }
  372. const folders = await response.json();
  373. availableFolders = Array.isArray(folders) ? folders : [];
  374. } catch (error) {
  375. console.warn("使用回退文件夹列表:", error.message);
  376. const maxCount = 3002;
  377. for (let i = 1; i <= maxCount; i++) {
  378. availableFolders.push(buildFolderName("player_", i, 4));
  379. }
  380. }
  381. updateNavDisabledState();
  382. await buildCards();
  383. if (!availableFolders.length) {
  384. resetStage();
  385. }
  386. }
  387. async function startLocalPreview(files, folderLabel) {
  388. log("🎬 开始本地预览,文件数:", files?.length, "文件夹:", folderLabel);
  389. if (!Array.isArray(files) || !files.length) {
  390. logWarn("文件数组为空或无效");
  391. showPlayerError("未检测到 PNG 图片");
  392. return;
  393. }
  394. log("文件列表:");
  395. files.forEach((file, idx) => {
  396. log(` [${idx}] ${file.name} - ${file.size} bytes`);
  397. });
  398. stopStageAnimation();
  399. showLoading(true);
  400. hidePlayerError();
  401. setStagePlaceholderVisible(false);
  402. cleanupLocalFrames();
  403. const orderedFiles = sortFilesForPlayback(files);
  404. log("排序后的文件数:", orderedFiles.length);
  405. localFrameResources = orderedFiles.map((file, index) => ({
  406. url: URL.createObjectURL(file),
  407. name: file.name || `frame_${index + 1}`,
  408. loaded: false,
  409. }));
  410. frameSourceMode = "local";
  411. frameList = localFrameResources;
  412. currentFrameCursor = 0;
  413. currentFolder = folderLabel || "本地导入";
  414. currentIndex = -1;
  415. if (folderNameLabel) {
  416. folderNameLabel.textContent = currentFolder;
  417. }
  418. if (folderCounterLabel) {
  419. folderCounterLabel.textContent = "-";
  420. }
  421. if (frameCounterLabel) {
  422. frameCounterLabel.textContent = `${localFrameResources.length} 帧`;
  423. }
  424. // 预加载所有图片后再开始播放
  425. try {
  426. log("开始预加载图片...");
  427. await preloadLocalFrames(localFrameResources);
  428. if (localFrameResources.length > 0) {
  429. log("✅ 预加载完成,开始播放");
  430. updateStageImage(localFrameResources[0]);
  431. showLoading(false);
  432. startStageLoop();
  433. } else {
  434. logWarn("预加载后资源为空");
  435. showLoading(false);
  436. showPlayerError("图片加载失败");
  437. }
  438. } catch (error) {
  439. logError("预加载图片失败:", error);
  440. showLoading(false);
  441. showPlayerError("图片加载失败");
  442. }
  443. }
  444. function preloadLocalFrames(frames) {
  445. return new Promise((resolve) => {
  446. if (!frames || !frames.length) {
  447. log("⚠️ 预加载:帧列表为空");
  448. resolve();
  449. return;
  450. }
  451. let loadedCount = 0;
  452. let hasError = false;
  453. const totalFrames = frames.length;
  454. log(`🖼️ 预加载 ${totalFrames} 个帧...`);
  455. frames.forEach((frame, index) => {
  456. const img = new Image();
  457. img.onload = () => {
  458. frame.loaded = true;
  459. loadedCount++;
  460. log(` ✅ [${loadedCount}/${totalFrames}] ${frame.name} 加载成功`);
  461. if (loadedCount === totalFrames) {
  462. log(`🎉 所有帧加载完成!`);
  463. resolve();
  464. }
  465. };
  466. img.onerror = () => {
  467. hasError = true;
  468. loadedCount++;
  469. logWarn(` ❌ [${loadedCount}/${totalFrames}] 帧 ${index + 1} 加载失败: ${frame.name}`);
  470. if (loadedCount === totalFrames) {
  471. logWarn(`预加载完成,但有 ${hasError ? '错误' : '部分失败'}`);
  472. resolve();
  473. }
  474. };
  475. log(` ⏳ 开始加载 [${index + 1}/${totalFrames}]: ${frame.name}`);
  476. img.src = frame.url;
  477. });
  478. // 超时保护:最多等待 10 秒
  479. setTimeout(() => {
  480. if (loadedCount < totalFrames) {
  481. logWarn(`⏰ 预加载超时,已加载 ${loadedCount}/${totalFrames} 帧`);
  482. resolve();
  483. }
  484. }, 10000);
  485. });
  486. }
  487. function sortFilesForPlayback(files) {
  488. return files
  489. .slice()
  490. .sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: "base" }));
  491. }
  492. function bindDropZone() {
  493. if (!playerShell) {
  494. return;
  495. }
  496. playerShell.addEventListener("dragenter", handleDropZoneDragEnter);
  497. playerShell.addEventListener("dragover", handleDropZoneDragOver);
  498. playerShell.addEventListener("dragleave", handleDropZoneDragLeave);
  499. playerShell.addEventListener("drop", handleDropZoneDrop);
  500. }
  501. function preventDragDefaults(event) {
  502. if (!event) {
  503. return;
  504. }
  505. event.preventDefault();
  506. event.stopPropagation();
  507. }
  508. function setDropZoneState(isActive) {
  509. if (!playerShell) {
  510. return;
  511. }
  512. playerShell.classList.toggle("is-dragging", Boolean(isActive));
  513. }
  514. function handleDropZoneDragEnter(event) {
  515. preventDragDefaults(event);
  516. setDropZoneState(true);
  517. }
  518. function handleDropZoneDragOver(event) {
  519. preventDragDefaults(event);
  520. setDropZoneState(true);
  521. }
  522. function handleDropZoneDragLeave(event) {
  523. preventDragDefaults(event);
  524. if (!playerShell) {
  525. return;
  526. }
  527. const related = event.relatedTarget;
  528. if (related && playerShell.contains(related)) {
  529. return;
  530. }
  531. setDropZoneState(false);
  532. }
  533. async function handleDropZoneDrop(event) {
  534. log("📥 Drop事件触发");
  535. preventDragDefaults(event);
  536. setDropZoneState(false);
  537. const transfer = event.dataTransfer;
  538. if (!transfer) {
  539. logWarn("dataTransfer 为空");
  540. return;
  541. }
  542. log("dataTransfer.items.length:", transfer.items?.length);
  543. log("dataTransfer.files.length:", transfer.files?.length);
  544. try {
  545. const { files, folderLabel } = await collectDroppedPngFiles(transfer);
  546. log("✅ 收集到的文件数:", files.length, "文件夹名:", folderLabel);
  547. if (!files.length) {
  548. logWarn("未检测到 PNG 图片");
  549. showPlayerError("未检测到 PNG 图片");
  550. return;
  551. }
  552. await startLocalPreview(files, folderLabel);
  553. } catch (error) {
  554. logError("拖放处理错误:", error);
  555. showPlayerError("读取文件夹失败");
  556. }
  557. }
  558. async function collectDroppedPngFiles(dataTransfer) {
  559. const items = Array.from((dataTransfer && dataTransfer.items) || []);
  560. log("🔍 开始收集文件,items数量:", items.length);
  561. let collected = [];
  562. if (items.length) {
  563. log("处理 dataTransfer.items...");
  564. const results = await Promise.all(items.map((item, idx) => {
  565. log(` - Item ${idx}: kind=${item.kind}, type=${item.type}`);
  566. return readDataTransferItem(item);
  567. }));
  568. collected = results.flat();
  569. log("从 items 收集到:", collected.length, "个条目");
  570. }
  571. if (!collected.length && dataTransfer && dataTransfer.files) {
  572. log("Items为空,尝试使用 dataTransfer.files...");
  573. collected = Array.from(dataTransfer.files).map((file) => {
  574. log(` - File: ${file.name}, type=${file.type}, size=${file.size}`);
  575. return { file };
  576. });
  577. log("从 files 收集到:", collected.length, "个文件");
  578. }
  579. log("过滤前总数:", collected.length);
  580. collected.forEach((entry, idx) => {
  581. if (entry.file) {
  582. log(` [${idx}] ${entry.file.name} - type: ${entry.file.type}, path: ${entry.file.webkitRelativePath || '(无路径)'}`);
  583. }
  584. });
  585. const pngEntries = collected.filter(({ file }) => {
  586. const isPng = isTopLevelPngFile(file);
  587. if (!isPng && file) {
  588. log(` ❌ 过滤掉: ${file.name} (type=${file.type})`);
  589. }
  590. return isPng;
  591. });
  592. log("✅ 过滤后 PNG 文件数:", pngEntries.length);
  593. return {
  594. files: pngEntries.map((entry) => entry.file),
  595. folderLabel: deriveFolderLabel(pngEntries),
  596. };
  597. }
  598. function isTopLevelPngFile(file) {
  599. if (!file) {
  600. log(" 🔍 isTopLevelPngFile: file 为空");
  601. return false;
  602. }
  603. const isPng = file.type === "image/png" || /\.png$/i.test(file.name || "");
  604. log(` 🔍 isTopLevelPngFile: ${file.name}, type="${file.type}", isPng=${isPng}`);
  605. if (!isPng) {
  606. return false;
  607. }
  608. if (!file.webkitRelativePath) {
  609. log(` ✅ 无相对路径,视为顶级文件`);
  610. return true;
  611. }
  612. const segments = file.webkitRelativePath.split("/").filter(Boolean);
  613. const isTopLevel = segments.length <= 2;
  614. log(` 📂 相对路径: ${file.webkitRelativePath}, 层级=${segments.length}, isTopLevel=${isTopLevel}`);
  615. return isTopLevel;
  616. }
  617. function deriveFolderLabel(entries) {
  618. if (!entries || !entries.length) {
  619. return "本地导入";
  620. }
  621. for (const entry of entries) {
  622. if (entry.rootName) {
  623. return entry.rootName;
  624. }
  625. const inferred = inferFolderFromFile(entry.file);
  626. if (inferred) {
  627. return inferred;
  628. }
  629. }
  630. const fallback = entries[0] && entries[0].file && entries[0].file.name;
  631. return fallback ? fallback.replace(/\.png$/i, "") : "本地导入";
  632. }
  633. function inferFolderFromFile(file) {
  634. if (!file || !file.webkitRelativePath) {
  635. return "";
  636. }
  637. const segments = file.webkitRelativePath.split("/").filter(Boolean);
  638. return segments.length ? segments[0] : "";
  639. }
  640. function inferFolderFromEntry(entry) {
  641. if (!entry || !entry.fullPath) {
  642. return "";
  643. }
  644. const segments = entry.fullPath.split("/").filter(Boolean);
  645. if (segments.length >= 2) {
  646. return segments[segments.length - 2];
  647. }
  648. return segments[0] || "";
  649. }
  650. function readDataTransferItem(item) {
  651. return new Promise((resolve) => {
  652. if (!item || item.kind !== "file") {
  653. log(" ⚠️ Item 不是文件类型");
  654. resolve([]);
  655. return;
  656. }
  657. const entry = item.webkitGetAsEntry ? item.webkitGetAsEntry() : null;
  658. if (!entry) {
  659. log(" ⚠️ 无法获取 entry,使用 getAsFile");
  660. const file = item.getAsFile();
  661. resolve(file ? [{ file }] : []);
  662. return;
  663. }
  664. log(` 📁 Entry: ${entry.name}, isFile=${entry.isFile}, isDirectory=${entry.isDirectory}`);
  665. if (entry.isFile) {
  666. entry.file(
  667. (file) => {
  668. log(` ✅ 读取文件成功: ${file.name}`);
  669. resolve(file ? [{ file, rootName: inferFolderFromEntry(entry) }] : []);
  670. },
  671. (err) => {
  672. logError(` ❌ 读取文件失败:`, err);
  673. resolve([]);
  674. }
  675. );
  676. return;
  677. }
  678. if (entry.isDirectory) {
  679. log(` 📂 开始读取目录: ${entry.name}`);
  680. readDirectoryImmediateFiles(entry).then((files) => {
  681. log(` ✅ 目录读取完成,文件数: ${files.length}`);
  682. resolve(
  683. files.map((file) => ({
  684. file,
  685. rootName: entry.name || inferFolderFromFile(file),
  686. }))
  687. );
  688. });
  689. return;
  690. }
  691. resolve([]);
  692. });
  693. }
  694. function readDirectoryImmediateFiles(directoryEntry) {
  695. return new Promise((resolve) => {
  696. if (!directoryEntry || !directoryEntry.isDirectory) {
  697. logWarn("directoryEntry 无效或不是目录");
  698. resolve([]);
  699. return;
  700. }
  701. const reader = directoryEntry.createReader();
  702. const files = [];
  703. let batchCount = 0;
  704. const readBatch = () => {
  705. reader.readEntries(
  706. (entries) => {
  707. batchCount++;
  708. log(` 📦 读取批次 ${batchCount}: ${entries.length} 个条目`);
  709. if (!entries.length) {
  710. log(` ✅ 目录读取完成,总文件数: ${files.length}`);
  711. resolve(files);
  712. return;
  713. }
  714. let pending = entries.length;
  715. entries.forEach((entry) => {
  716. if (entry.isFile) {
  717. log(` 📄 文件: ${entry.name}`);
  718. entry.file(
  719. (file) => {
  720. if (file) {
  721. log(` ✅ 文件读取成功: ${file.name}, size=${file.size}, type=${file.type}`);
  722. files.push(file);
  723. }
  724. pending -= 1;
  725. if (pending === 0) {
  726. readBatch();
  727. }
  728. },
  729. (err) => {
  730. logError(` ❌ 文件读取失败: ${entry.name}`, err);
  731. pending -= 1;
  732. if (pending === 0) {
  733. readBatch();
  734. }
  735. }
  736. );
  737. } else {
  738. log(` 📁 跳过子目录: ${entry.name}`);
  739. // skip subdirectories completely
  740. pending -= 1;
  741. if (pending === 0) {
  742. readBatch();
  743. }
  744. }
  745. });
  746. },
  747. (err) => {
  748. logError(" ❌ readEntries 失败:", err);
  749. resolve(files);
  750. }
  751. );
  752. };
  753. readBatch();
  754. });
  755. }
  756. function bindControls() {
  757. if (fpsSlider && fpsValue) {
  758. fpsSlider.value = currentFps;
  759. fpsValue.textContent = `${currentFps} FPS`;
  760. fpsSlider.addEventListener("input", () => {
  761. const value = parseInt(fpsSlider.value, 10) || currentFps;
  762. setFps(value);
  763. fpsValue.textContent = `${value} FPS`;
  764. });
  765. }
  766. if (prevBtn) {
  767. prevBtn.addEventListener("click", () => selectCardByIndex(currentIndex - 1));
  768. }
  769. if (nextBtn) {
  770. nextBtn.addEventListener("click", () => selectCardByIndex(currentIndex + 1));
  771. }
  772. if (downloadBtn) {
  773. downloadBtn.addEventListener("click", () => {
  774. if (!currentFolder) {
  775. return;
  776. }
  777. if (window.SpriteSheetMaker && typeof window.SpriteSheetMaker.handleDownloadClick === "function") {
  778. window.SpriteSheetMaker.handleDownloadClick(currentFolder, currentIndex);
  779. }
  780. });
  781. }
  782. document.addEventListener("keydown", (event) => {
  783. if (event.key === "ArrowLeft") {
  784. selectCardByIndex(currentIndex - 1);
  785. } else if (event.key === "ArrowRight") {
  786. selectCardByIndex(currentIndex + 1);
  787. }
  788. });
  789. bindDropZone();
  790. }
  791. let previewCard = null;
  792. window.addEventListener("DOMContentLoaded", async () => {
  793. // 初始化预览卡片
  794. const container = document.getElementById('previewCardContainer');
  795. if (container && window.PreviewCard) {
  796. try {
  797. previewCard = new window.PreviewCard(container, {
  798. fps: 8,
  799. onFpsChange: (fps) => {
  800. // log('FPS changed to:', fps);
  801. setFps(fps);
  802. }
  803. });
  804. // log('✅ PreviewCard initialized');
  805. } catch (error) {
  806. logError('Failed to initialize PreviewCard:', error);
  807. }
  808. }
  809. bindControls();
  810. loadAvailableFolders();
  811. });
  812. window.addEventListener("beforeunload", cleanupLocalFrames);
  813. })();