Files
SpineViewer/SpineThumbnailGenerateServer/js/library-watcher.js
T
2026-11-06 14:11:49 +08:00

122 lines
4.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 || (() => {});
const t = options.t || ((key) => key);
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(t('thumb.watchReadFailed', { error: (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(t('thumb.watchMissingDir', { path: imagesDir }));
return false;
}
known = listItemDirs();
try {
watcher = fs.watch(imagesDir, { persistent: true }, () => scheduleFlush());
} catch (error) {
onLog(t('thumb.watchFailed', { error: (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;
})();