Files
SpineViewer/js/spine-group-thumbnails.js
T
2026-09-17 18:40:29 +08:00

76 lines
3.3 KiB
JavaScript

'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 || [];
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 };