122 lines
4.5 KiB
JavaScript
122 lines
4.5 KiB
JavaScript
// 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;
|
||
|
||
})();
|