干净发布副本

This commit is contained in:
tianmo committed 2026-10-28 19:19:04 +08:00
1 parent c29de7a26b
commit acc93d5404
35 files changed
+82427

No files matched your search

@@ -0,0 +1,121 @@
// Node 里用 require(跑测试),插件页面里用 <script> 标签加载;包成 IIFE 是为了
// 让多个页面脚本各自的 const 不互相冲突。
(function () {
'use strict';
const fs = require('fs');
const path = require('path');
// Eagle 没有暴露「素材导入完成」事件,所以用「目录静默检测」代替:
// 监听素材目录,任何变化都重置防抖计时器,安静 QUIET_MS 之后才认为这一批拖入结束。
//
// 1 秒:足够合并连续写入。Eagle 的「重复添加提示」是主窗口里的 Angular 模态框,
// 没有独立窗口句柄可检测,插件只能靠静默判断——那期间素材库是静止的,但重复文件
// 还没入库,扫到的只是已入库的非重复素材。
const QUIET_MS = 1000;
// fs.watch 在网络盘/外置盘上可能漏事件,用低频兜底扫描补上。
const FALLBACK_MS = 30000;
/** 删除指定目录中超过保留时间的本插件临时文件。 */
function cleanupStaleFiles(directory, prefix, maxAgeMs, now = Date.now()) {
let names;
try { names = fs.readdirSync(directory); } catch (error) { return 0; }
let removed = 0;
for (const name of names) {
if (!name.startsWith(prefix)) continue;
const file = path.join(directory, name);
try {
const stat = fs.lstatSync(file);
if (!stat.isFile() || now - stat.mtimeMs < maxAgeMs) continue;
fs.unlinkSync(file);
removed += 1;
} catch (error) { /* 单个文件失败不影响其余清理和监听 */ }
}
return removed;
}
function createLibraryWatcher(options) {
const onBatch = options.onBatch || (() => {});
const onLog = options.onLog || (() => {});
let imagesDir = null;
let watcher = null;
let quietTimer = null;
let fallbackTimer = null;
let known = null;
function listItemDirs() {
try {
return new Set(fs.readdirSync(imagesDir).filter((name) => name.endsWith('.info')));
} catch (error) {
onLog('读取素材目录失败:' + ((error && error.message) || error));
return new Set();
}
}
/** 与上次快照比对,把新增的素材目录交给上层处理。 */
function flush(reason) {
if (!imagesDir || !known) return;
const current = listItemDirs();
const added = [...current].filter((name) => !known.has(name));
known = current;
onBatch({ added, total: current.size, reason });
}
function scheduleFlush() {
if (quietTimer) clearTimeout(quietTimer);
quietTimer = setTimeout(() => {
quietTimer = null;
flush('quiet');
}, QUIET_MS);
}
function stop() {
if (watcher) { watcher.close(); watcher = null; }
if (quietTimer) { clearTimeout(quietTimer); quietTimer = null; }
if (fallbackTimer) { clearInterval(fallbackTimer); fallbackTimer = null; }
imagesDir = null;
known = null;
}
/** 监听指定素材库。首次快照里的素材视为历史数据,不处理。 */
function start(libraryPath) {
stop();
imagesDir = path.join(libraryPath, 'images');
if (!fs.existsSync(imagesDir)) {
onLog('素材目录不存在:' + imagesDir);
return false;
}
known = listItemDirs();
onLog('监听 ' + imagesDir + '(现有 ' + known.size + ' 个素材视为历史,不处理)');
try {
watcher = fs.watch(imagesDir, { persistent: true }, () => scheduleFlush());
} catch (error) {
onLog('建立监听失败:' + ((error && error.message) || error));
return false;
}
fallbackTimer = setInterval(() => {
const current = listItemDirs();
// 不能只比数量:同一次导里如果有增有删,数量可能刚好抵消。
let changed = current.size !== known.size;
if (!changed) {
for (const name of current) {
if (!known.has(name)) { changed = true; break; }
}
}
if (changed) flush('fallback');
}, FALLBACK_MS);
return true;
}
return { start, stop, flush, isRunning: () => !!watcher };
}
// 同时支持两种加载方式:Node 里用 require(跑测试),插件页面里用 <script> 标签。
// service 插件页面的 require 解析基准不是插件目录,所以页面里必须走 script 标签。
const libraryWatcherApi = { createLibraryWatcher, cleanupStaleFiles, QUIET_MS, FALLBACK_MS };
// 优先挂 window:Eagle 插件页面里也存在 module 对象(它有 Node 模块系统),
// 先判断 module 会让页面拿到模块却取不到 window 上的名字。Node 里没有 window,自然走 require 分支。
if (typeof window !== 'undefined') window.SpineThumbLibraryWatcher = libraryWatcherApi;
else if (typeof module !== 'undefined' && module.exports) module.exports = libraryWatcherApi;
})();