'use strict'; const crypto = require('crypto'); const fs = require('fs'); const os = require('os'); const path = require('path'); function orderCurrentTargetLast(targets, currentSourcePath) { if (!currentSourcePath) return [...targets]; const current = path.resolve(currentSourcePath); return [...targets].sort((left, right) => { const leftIsCurrent = path.resolve(left.sourcePath) === current; const rightIsCurrent = path.resolve(right.sourcePath) === current; return Number(leftIsCurrent) - Number(rightIsCurrent); }); } /** * Apply one rendered frame to explicitly selected Eagle items. * Each item gets an independent temporary file because Eagle may consume or * defer reading the file passed to setCustomThumbnail. */ async function applyThumbnailTargets(options) { const targets = options.targets || []; // 渲染结果为空时立刻报清楚的原因,否则会在写临时文件时才抛出难懂的参数类型错误。 if (!options.rendered || !Buffer.isBuffer(options.rendered.buffer)) throw new Error('缩略图渲染结果为空'); const settleDelayMs = options.settleDelayMs === undefined ? 800 : options.settleDelayMs; const cleanupDelayMs = options.cleanupDelayMs === undefined ? 1200 : options.cleanupDelayMs; const retryAttempts = options.retryAttempts === undefined ? 3 : options.retryAttempts; const delay = (ms) => ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve(); const tempPaths = []; let success = 0; let skipped = Number(options.initialSkipped) || 0; let failed = 0; for (let index = 0; index < targets.length; index++) { if (options.onProgress) options.onProgress(index + 1, targets.length); let itemTempPath = null; try { const target = targets[index]; const fresh = await options.getItem(target.sourcePath); if (fresh.data.customThumbnail === true && target.allowOverwrite !== true) { skipped++; continue; } itemTempPath = path.join(os.tmpdir(), 'spineviewer-group-' + fresh.id + '-' + crypto.randomBytes(12).toString('hex') + options.rendered.extension); fs.writeFileSync(itemTempPath, options.rendered.buffer); tempPaths.push(itemTempPath); let applied = false; let lastError = null; for (let attempt = 0; attempt < retryAttempts && !applied; attempt++) { try { const accepted = await options.setThumbnail(fresh.id, itemTempPath); if (accepted !== true) throw new Error(options.notConfirmedMessage || 'Thumbnail update was rejected'); applied = await options.waitForApplied(target.sourcePath); if (!applied) throw new Error(options.notConfirmedMessage || 'Thumbnail update was not confirmed'); } catch (error) { lastError = error; if (attempt < retryAttempts - 1) await delay(settleDelayMs); } } if (!applied) throw lastError || new Error(options.notConfirmedMessage || 'Thumbnail update was not confirmed'); // The local HTTP API can resolve before Eagle has finished consuming the // supplied file. Keep every file alive and serialize requests with a short // settling interval so a later request cannot replace an earlier queued job. await delay(settleDelayMs); success++; if (options.onSuccess) options.onSuccess(fresh); } catch (error) { failed++; if (options.onError) options.onError(error); } finally { /* temporary inputs are retained until the whole queue settles */ } } await delay(cleanupDelayMs); for (const tempPath of tempPaths) { try { if (fs.existsSync(tempPath)) fs.unlinkSync(tempPath); } catch (e) { /* ignore */ } } return { success, skipped, failed }; } module.exports = { applyThumbnailTargets, orderCurrentTargetLast };