// PNG 序列播放工具 - 主逻辑 // 负责动画播放、帧管理等功能 (function () { // 调试日志开关 const DEBUG = true; function log(...args) { if (DEBUG) console.log("[SeqAniPlayer]", ...args); } function logError(...args) { console.error("[SeqAniPlayer ERROR]", ...args); } function logWarn(...args) { console.warn("[SeqAniPlayer WARN]", ...args); } const foldersApi = "http://localhost:3000/api/folders"; // 获取当前登录用户名 let currentUsername = null; function getCurrentUsername() { if (currentUsername) { return currentUsername; } try { const loginDataStr = localStorage.getItem('loginData'); if (!loginDataStr) { return null; } const loginData = JSON.parse(loginDataStr); const now = Date.now(); // 检查是否过期 if (now >= loginData.expireTime) { localStorage.removeItem('loginData'); return null; } currentUsername = loginData.user ? loginData.user.username : null; return currentUsername; } catch (error) { console.error('[SeqAniPlayer] 获取用户名失败:', error); return null; } } const fpsSlider = document.getElementById("fpsSlider"); const fpsValue = document.getElementById("fpsValue"); const cardsGrid = document.getElementById("cardsGrid"); const cardCountLabel = document.getElementById("cardCount"); const folderNameLabel = document.getElementById("folderName"); const folderCounterLabel = document.getElementById("folderCounter"); const frameCounterLabel = document.getElementById("frameCounter"); const downloadBtn = document.getElementById("downloadBtn"); const prevBtn = document.getElementById("prevFolderBtn"); const nextBtn = document.getElementById("nextFolderBtn"); const playerImage = document.getElementById("playerImage"); const playerLoadingOverlay = document.getElementById("playerLoadingOverlay"); const playerError = document.getElementById("playerError"); const playerShell = document.querySelector(".player-image-shell"); const dropHint = document.getElementById("dropHint"); let availableFolders = []; let cards = []; let cardTemplate = null; let cardTemplatePromise = null; let currentIndex = -1; let currentFps = 8; let currentFolder = ""; let frameList = []; let currentFrameCursor = 0; let stageTimer = null; let frameSourceMode = "remote"; let localFrameResources = []; function padFrame(index) { return index.toString().padStart(2, "0"); } function buildFrameSrc(folder, index) { const frameName = padFrame(index); const username = getCurrentUsername(); if (!username) { logWarn('未登录,无法加载图片'); return ''; } // 使用API路径,从用户目录加载 const imagePath = `${folder}/${frameName}.png`; return `/api/disk/preview?username=${encodeURIComponent(username)}&path=${encodeURIComponent(imagePath)}`; } function buildFolderName(prefix, index, padding) { const num = index.toString().padStart(padding, "0"); return `${prefix}${num}`; } async function ensureCardTemplate() { if (cardTemplate) { return cardTemplate; } if (cardTemplatePromise) { return cardTemplatePromise; } cardTemplatePromise = fetch("./card.html") .then((response) => { if (!response.ok) { throw new Error("Failed to load card template"); } return response.text(); }) .then((html) => { const wrapper = document.createElement("div"); wrapper.innerHTML = html.trim(); const template = wrapper.querySelector("#card-template"); if (!template) { throw new Error("Card template missing"); } cardTemplate = template; return cardTemplate; }) .catch((error) => { console.error(error); cardTemplatePromise = null; throw error; }); return cardTemplatePromise; } function destroyCards() { cards.forEach((card) => card.destroy && card.destroy()); cards = []; if (cardsGrid) { cardsGrid.innerHTML = ""; } } function updateCardCount() { if (!cardCountLabel) return; cardCountLabel.textContent = `${availableFolders.length} 个动画`; } function updateNavDisabledState() { const disabled = availableFolders.length <= 1; if (prevBtn) prevBtn.disabled = disabled; if (nextBtn) nextBtn.disabled = disabled; } function updateMeta() { const total = availableFolders.length; const safeIndex = currentIndex >= 0 ? currentIndex : 0; const folderName = availableFolders[safeIndex] || "--"; if (folderNameLabel) { folderNameLabel.textContent = folderName; } if (folderCounterLabel) { folderCounterLabel.textContent = total ? `${safeIndex + 1} / ${total}` : "0 / 0"; } } function showLoading(isLoading) { if (!playerLoadingOverlay) return; playerLoadingOverlay.classList.toggle("is-visible", isLoading); } function showPlayerError(message) { if (!playerError) return; playerError.textContent = message; playerError.hidden = false; } function hidePlayerError() { if (!playerError) return; playerError.hidden = true; } function stopStageAnimation() { if (stageTimer) { clearInterval(stageTimer); stageTimer = null; } } function setStagePlaceholderVisible(isVisible) { if (!playerImage) { return; } playerImage.classList.toggle("is-hidden", Boolean(isVisible)); if (isVisible) { playerImage.removeAttribute("src"); } } function cleanupLocalFrames() { if (!localFrameResources.length) { return; } localFrameResources.forEach((frame) => { if (frame && frame.url) { URL.revokeObjectURL(frame.url); } }); localFrameResources = []; } function switchToRemoteSource() { if (frameSourceMode !== "remote") { cleanupLocalFrames(); frameSourceMode = "remote"; } } function startStageLoop() { log("🎬 启动动画循环"); stopStageAnimation(); if (!frameList.length) { logWarn("帧列表为空,无法启动循环"); return; } const interval = 1000 / currentFps; log(`⏱️ 动画循环启动,帧数: ${frameList.length}, FPS: ${currentFps}, 间隔: ${interval}ms`); stageTimer = setInterval(() => { if (!frameList.length) { stopStageAnimation(); return; } currentFrameCursor = (currentFrameCursor + 1) % frameList.length; updateStageImage(frameList[currentFrameCursor]); }, interval); } function updateStageImage(frameData) { if (!playerImage) { logWarn("playerImage 元素不存在"); return; } if (frameSourceMode === "local") { const frame = typeof frameData === "object" ? frameData : frameList[frameData]; if (!frame || !frame.url) { logWarn("本地模式:帧数据无效"); return; } setStagePlaceholderVisible(false); if (playerImage.src !== frame.url) { log(`🖼️ 更新图片: ${frame.name}`); playerImage.src = frame.url; } return; } if (!currentFolder) { logWarn("远程模式:文件夹未设置"); return; } const frameNumber = typeof frameData === "number" ? frameData : parseInt(frameData, 10); if (Number.isNaN(frameNumber)) { logWarn("远程模式:帧编号无效"); return; } const nextSrc = buildFrameSrc(currentFolder, frameNumber); setStagePlaceholderVisible(false); if (playerImage.src !== nextSrc) { playerImage.src = nextSrc; } } function setFps(fps) { currentFps = fps; if (frameList.length > 0) { startStageLoop(); } } async function buildCards() { if (!cardsGrid || !window.SequenceCard) { return; } const template = await ensureCardTemplate(); destroyCards(); availableFolders.forEach((folderName, index) => { const templateContent = template.content.firstElementChild; if (!templateContent) { return; } const cardElement = templateContent.cloneNode(true); cardsGrid.appendChild(cardElement); const cardInstance = new window.SequenceCard( cardElement, folderName, index, buildFrameSrc, handleCardSelect ); cardInstance.loadPreview(); cards.push(cardInstance); }); updateCardCount(); } function handleCardSelect(folderName, cardIndex) { if (typeof cardIndex !== "number") { return; } selectCardByIndex(cardIndex); } function highlightActiveCard() { cards.forEach((card) => { if (typeof card.setActive === "function") { card.setActive(card.index === currentIndex); } }); } function resetStage() { stopStageAnimation(); switchToRemoteSource(); frameList = []; currentFolder = ""; currentFrameCursor = 0; showLoading(false); hidePlayerError(); setStagePlaceholderVisible(true); if (frameCounterLabel) { frameCounterLabel.textContent = "0 帧"; } if (folderNameLabel) { folderNameLabel.textContent = "--"; } if (folderCounterLabel) { folderCounterLabel.textContent = "0 / 0"; } } async function selectCardByIndex(index) { if (!availableFolders.length) { resetStage(); return; } const total = availableFolders.length; currentIndex = ((index % total) + total) % total; currentFolder = availableFolders[currentIndex]; updateMeta(); highlightActiveCard(); await startStageForFolder(currentFolder); } function sanitizeFrameList(frameInfo) { if (frameInfo && Array.isArray(frameInfo.frames) && frameInfo.frames.length > 0) { return frameInfo.frames; } const maxFrame = frameInfo && frameInfo.maxFrame ? frameInfo.maxFrame : 0; if (!maxFrame) { return []; } return Array.from({ length: maxFrame }, (_, idx) => idx + 1); } async function startStageForFolder(folderName) { stopStageAnimation(); switchToRemoteSource(); frameList = []; currentFrameCursor = 0; showLoading(true); hidePlayerError(); if (frameCounterLabel) { frameCounterLabel.textContent = "0 帧"; } if (!folderName) { showLoading(false); showPlayerError("未选择动画"); return; } try { const response = await fetch(`http://localhost:3000/api/frames/${folderName}`); if (!response.ok) { throw new Error("Failed to fetch frames"); } const data = await response.json(); frameList = sanitizeFrameList(data); if (!frameList.length) { showPlayerError("暂无可用帧"); setStagePlaceholderVisible(true); showLoading(false); return; } if (frameCounterLabel) { frameCounterLabel.textContent = `${frameList.length} 帧`; } updateStageImage(frameList[0]); showLoading(false); startStageLoop(); } catch (error) { console.error(error); showLoading(false); showPlayerError("加载失败"); } } async function loadAvailableFolders() { availableFolders = []; try { const response = await fetch(foldersApi); if (!response.ok) { throw new Error("Failed to fetch folders"); } const folders = await response.json(); availableFolders = Array.isArray(folders) ? folders : []; } catch (error) { console.warn("使用回退文件夹列表:", error.message); const maxCount = 3002; for (let i = 1; i <= maxCount; i++) { availableFolders.push(buildFolderName("player_", i, 4)); } } updateNavDisabledState(); await buildCards(); if (!availableFolders.length) { resetStage(); } } async function startLocalPreview(files, folderLabel) { log("🎬 开始本地预览,文件数:", files?.length, "文件夹:", folderLabel); if (!Array.isArray(files) || !files.length) { logWarn("文件数组为空或无效"); showPlayerError("未检测到 PNG 图片"); return; } log("文件列表:"); files.forEach((file, idx) => { log(` [${idx}] ${file.name} - ${file.size} bytes`); }); stopStageAnimation(); showLoading(true); hidePlayerError(); setStagePlaceholderVisible(false); cleanupLocalFrames(); const orderedFiles = sortFilesForPlayback(files); log("排序后的文件数:", orderedFiles.length); localFrameResources = orderedFiles.map((file, index) => ({ url: URL.createObjectURL(file), name: file.name || `frame_${index + 1}`, loaded: false, })); frameSourceMode = "local"; frameList = localFrameResources; currentFrameCursor = 0; currentFolder = folderLabel || "本地导入"; currentIndex = -1; if (folderNameLabel) { folderNameLabel.textContent = currentFolder; } if (folderCounterLabel) { folderCounterLabel.textContent = "-"; } if (frameCounterLabel) { frameCounterLabel.textContent = `${localFrameResources.length} 帧`; } // 预加载所有图片后再开始播放 try { log("开始预加载图片..."); await preloadLocalFrames(localFrameResources); if (localFrameResources.length > 0) { log("✅ 预加载完成,开始播放"); updateStageImage(localFrameResources[0]); showLoading(false); startStageLoop(); } else { logWarn("预加载后资源为空"); showLoading(false); showPlayerError("图片加载失败"); } } catch (error) { logError("预加载图片失败:", error); showLoading(false); showPlayerError("图片加载失败"); } } function preloadLocalFrames(frames) { return new Promise((resolve) => { if (!frames || !frames.length) { log("⚠️ 预加载:帧列表为空"); resolve(); return; } let loadedCount = 0; let hasError = false; const totalFrames = frames.length; log(`🖼️ 预加载 ${totalFrames} 个帧...`); frames.forEach((frame, index) => { const img = new Image(); img.onload = () => { frame.loaded = true; loadedCount++; log(` ✅ [${loadedCount}/${totalFrames}] ${frame.name} 加载成功`); if (loadedCount === totalFrames) { log(`🎉 所有帧加载完成!`); resolve(); } }; img.onerror = () => { hasError = true; loadedCount++; logWarn(` ❌ [${loadedCount}/${totalFrames}] 帧 ${index + 1} 加载失败: ${frame.name}`); if (loadedCount === totalFrames) { logWarn(`预加载完成,但有 ${hasError ? '错误' : '部分失败'}`); resolve(); } }; log(` ⏳ 开始加载 [${index + 1}/${totalFrames}]: ${frame.name}`); img.src = frame.url; }); // 超时保护:最多等待 10 秒 setTimeout(() => { if (loadedCount < totalFrames) { logWarn(`⏰ 预加载超时,已加载 ${loadedCount}/${totalFrames} 帧`); resolve(); } }, 10000); }); } function sortFilesForPlayback(files) { return files .slice() .sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: "base" })); } function bindDropZone() { if (!playerShell) { return; } playerShell.addEventListener("dragenter", handleDropZoneDragEnter); playerShell.addEventListener("dragover", handleDropZoneDragOver); playerShell.addEventListener("dragleave", handleDropZoneDragLeave); playerShell.addEventListener("drop", handleDropZoneDrop); } function preventDragDefaults(event) { if (!event) { return; } event.preventDefault(); event.stopPropagation(); } function setDropZoneState(isActive) { if (!playerShell) { return; } playerShell.classList.toggle("is-dragging", Boolean(isActive)); } function handleDropZoneDragEnter(event) { preventDragDefaults(event); setDropZoneState(true); } function handleDropZoneDragOver(event) { preventDragDefaults(event); setDropZoneState(true); } function handleDropZoneDragLeave(event) { preventDragDefaults(event); if (!playerShell) { return; } const related = event.relatedTarget; if (related && playerShell.contains(related)) { return; } setDropZoneState(false); } async function handleDropZoneDrop(event) { log("📥 Drop事件触发"); preventDragDefaults(event); setDropZoneState(false); const transfer = event.dataTransfer; if (!transfer) { logWarn("dataTransfer 为空"); return; } log("dataTransfer.items.length:", transfer.items?.length); log("dataTransfer.files.length:", transfer.files?.length); try { const { files, folderLabel } = await collectDroppedPngFiles(transfer); log("✅ 收集到的文件数:", files.length, "文件夹名:", folderLabel); if (!files.length) { logWarn("未检测到 PNG 图片"); showPlayerError("未检测到 PNG 图片"); return; } await startLocalPreview(files, folderLabel); } catch (error) { logError("拖放处理错误:", error); showPlayerError("读取文件夹失败"); } } async function collectDroppedPngFiles(dataTransfer) { const items = Array.from((dataTransfer && dataTransfer.items) || []); log("🔍 开始收集文件,items数量:", items.length); let collected = []; if (items.length) { log("处理 dataTransfer.items..."); const results = await Promise.all(items.map((item, idx) => { log(` - Item ${idx}: kind=${item.kind}, type=${item.type}`); return readDataTransferItem(item); })); collected = results.flat(); log("从 items 收集到:", collected.length, "个条目"); } if (!collected.length && dataTransfer && dataTransfer.files) { log("Items为空,尝试使用 dataTransfer.files..."); collected = Array.from(dataTransfer.files).map((file) => { log(` - File: ${file.name}, type=${file.type}, size=${file.size}`); return { file }; }); log("从 files 收集到:", collected.length, "个文件"); } log("过滤前总数:", collected.length); collected.forEach((entry, idx) => { if (entry.file) { log(` [${idx}] ${entry.file.name} - type: ${entry.file.type}, path: ${entry.file.webkitRelativePath || '(无路径)'}`); } }); const pngEntries = collected.filter(({ file }) => { const isPng = isTopLevelPngFile(file); if (!isPng && file) { log(` ❌ 过滤掉: ${file.name} (type=${file.type})`); } return isPng; }); log("✅ 过滤后 PNG 文件数:", pngEntries.length); return { files: pngEntries.map((entry) => entry.file), folderLabel: deriveFolderLabel(pngEntries), }; } function isTopLevelPngFile(file) { if (!file) { log(" 🔍 isTopLevelPngFile: file 为空"); return false; } const isPng = file.type === "image/png" || /\.png$/i.test(file.name || ""); log(` 🔍 isTopLevelPngFile: ${file.name}, type="${file.type}", isPng=${isPng}`); if (!isPng) { return false; } if (!file.webkitRelativePath) { log(` ✅ 无相对路径,视为顶级文件`); return true; } const segments = file.webkitRelativePath.split("/").filter(Boolean); const isTopLevel = segments.length <= 2; log(` 📂 相对路径: ${file.webkitRelativePath}, 层级=${segments.length}, isTopLevel=${isTopLevel}`); return isTopLevel; } function deriveFolderLabel(entries) { if (!entries || !entries.length) { return "本地导入"; } for (const entry of entries) { if (entry.rootName) { return entry.rootName; } const inferred = inferFolderFromFile(entry.file); if (inferred) { return inferred; } } const fallback = entries[0] && entries[0].file && entries[0].file.name; return fallback ? fallback.replace(/\.png$/i, "") : "本地导入"; } function inferFolderFromFile(file) { if (!file || !file.webkitRelativePath) { return ""; } const segments = file.webkitRelativePath.split("/").filter(Boolean); return segments.length ? segments[0] : ""; } function inferFolderFromEntry(entry) { if (!entry || !entry.fullPath) { return ""; } const segments = entry.fullPath.split("/").filter(Boolean); if (segments.length >= 2) { return segments[segments.length - 2]; } return segments[0] || ""; } function readDataTransferItem(item) { return new Promise((resolve) => { if (!item || item.kind !== "file") { log(" ⚠️ Item 不是文件类型"); resolve([]); return; } const entry = item.webkitGetAsEntry ? item.webkitGetAsEntry() : null; if (!entry) { log(" ⚠️ 无法获取 entry,使用 getAsFile"); const file = item.getAsFile(); resolve(file ? [{ file }] : []); return; } log(` 📁 Entry: ${entry.name}, isFile=${entry.isFile}, isDirectory=${entry.isDirectory}`); if (entry.isFile) { entry.file( (file) => { log(` ✅ 读取文件成功: ${file.name}`); resolve(file ? [{ file, rootName: inferFolderFromEntry(entry) }] : []); }, (err) => { logError(` ❌ 读取文件失败:`, err); resolve([]); } ); return; } if (entry.isDirectory) { log(` 📂 开始读取目录: ${entry.name}`); readDirectoryImmediateFiles(entry).then((files) => { log(` ✅ 目录读取完成,文件数: ${files.length}`); resolve( files.map((file) => ({ file, rootName: entry.name || inferFolderFromFile(file), })) ); }); return; } resolve([]); }); } function readDirectoryImmediateFiles(directoryEntry) { return new Promise((resolve) => { if (!directoryEntry || !directoryEntry.isDirectory) { logWarn("directoryEntry 无效或不是目录"); resolve([]); return; } const reader = directoryEntry.createReader(); const files = []; let batchCount = 0; const readBatch = () => { reader.readEntries( (entries) => { batchCount++; log(` 📦 读取批次 ${batchCount}: ${entries.length} 个条目`); if (!entries.length) { log(` ✅ 目录读取完成,总文件数: ${files.length}`); resolve(files); return; } let pending = entries.length; entries.forEach((entry) => { if (entry.isFile) { log(` 📄 文件: ${entry.name}`); entry.file( (file) => { if (file) { log(` ✅ 文件读取成功: ${file.name}, size=${file.size}, type=${file.type}`); files.push(file); } pending -= 1; if (pending === 0) { readBatch(); } }, (err) => { logError(` ❌ 文件读取失败: ${entry.name}`, err); pending -= 1; if (pending === 0) { readBatch(); } } ); } else { log(` 📁 跳过子目录: ${entry.name}`); // skip subdirectories completely pending -= 1; if (pending === 0) { readBatch(); } } }); }, (err) => { logError(" ❌ readEntries 失败:", err); resolve(files); } ); }; readBatch(); }); } function bindControls() { if (fpsSlider && fpsValue) { fpsSlider.value = currentFps; fpsValue.textContent = `${currentFps} FPS`; fpsSlider.addEventListener("input", () => { const value = parseInt(fpsSlider.value, 10) || currentFps; setFps(value); fpsValue.textContent = `${value} FPS`; }); } if (prevBtn) { prevBtn.addEventListener("click", () => selectCardByIndex(currentIndex - 1)); } if (nextBtn) { nextBtn.addEventListener("click", () => selectCardByIndex(currentIndex + 1)); } if (downloadBtn) { downloadBtn.addEventListener("click", () => { if (!currentFolder) { return; } if (window.SpriteSheetMaker && typeof window.SpriteSheetMaker.handleDownloadClick === "function") { window.SpriteSheetMaker.handleDownloadClick(currentFolder, currentIndex); } }); } document.addEventListener("keydown", (event) => { if (event.key === "ArrowLeft") { selectCardByIndex(currentIndex - 1); } else if (event.key === "ArrowRight") { selectCardByIndex(currentIndex + 1); } }); bindDropZone(); } let previewCard = null; window.addEventListener("DOMContentLoaded", async () => { // 初始化预览卡片 const container = document.getElementById('previewCardContainer'); if (container && window.PreviewCard) { try { previewCard = new window.PreviewCard(container, { fps: 8, onFpsChange: (fps) => { // log('FPS changed to:', fps); setFps(fps); } }); // log('✅ PreviewCard initialized'); } catch (error) { logError('Failed to initialize PreviewCard:', error); } } bindControls(); loadAvailableFolders(); }); window.addEventListener("beforeunload", cleanupLocalFrames); })();