自动缩略图插件

This commit is contained in:
Yeomai committed 2026-09-20 23:29:13 +08:00
1 parent 96a0db09d8
commit 07c21f5954
5 files changed
+474 -8

No files matched your search

+202
View File
@@ -0,0 +1,202 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Spine缩略图</title>
<style>
:root { --bg: #1e1e1e; --panel: #252526; --text: #e6e6e6; --dim: #9d9d9d; --accent: #4fc1ff; --border: #3c3c3c; }
html, body { margin: 0; background: var(--bg); color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Microsoft YaHei", sans-serif; font-size: 13px; }
body { display: flex; flex-direction: column; gap: 8px; padding: 14px; box-sizing: border-box; }
h1 { font-size: 13px; margin: 0; font-weight: 600; text-align: center; }
.bar { height: 10px; background: var(--panel); border: 1px solid var(--border); border-radius: 999px; overflow: hidden; }
/* 不加过渡:窗口被遮挡时 CSS 过渡会停在中途,进度条就不跟着走了。 */
.bar > i { display: block; height: 100%; width: 0; background: var(--accent); }
#count { color: var(--text); }
#name { color: var(--dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
button { background: var(--panel); color: var(--text); border: 1px solid var(--border);
border-radius: 6px; padding: 5px 24px; font: inherit; cursor: pointer; }
button:hover { border-color: var(--accent); color: var(--accent); }
#action { align-self: center; }
#confirm { display: none; flex-direction: column; gap: 8px; }
#confirmText { text-align: center; color: var(--text); }
#confirmButtons { display: flex; gap: 10px; justify-content: center; }
/* 继续 = 默认项(绿色、自动获得焦点);确认取消 = 危险操作(红色)。 */
#resume { background: #2ea043; border-color: #2ea043; color: #fff; }
#resume:hover { background: #3fb950; border-color: #3fb950; color: #fff; }
#cancel { background: #d64545; border-color: #d64545; color: #fff; }
#cancel:hover { background: #e5534b; border-color: #e5534b; color: #fff; }
</style>
</head>
<body>
<h1>正在生成动态缩略图……</h1>
<div class="bar"><i id="bar"></i></div>
<div id="count">等待开始…</div>
<div id="name"></div>
<div id="confirm">
<div id="confirmText">是否确认取消?</div>
<div id="confirmButtons">
<button id="resume" type="button">继续</button>
<button id="cancel" type="button">确认取消</button>
</div>
</div>
<button id="action" type="button">停止</button>
<script>
// 这个页面是宿主用 Eagle 的 create-plugin-window 打开的插件窗口:
// 无标题栏、置顶、挂在 Eagle 主窗口上;进度从临时目录的状态文件轮询。
// 按钮往回写控制文件:生成中 = 停止(暂停并就地询问),生成完 = 确认(关窗)。
const fs = require('fs');
const os = require('os');
const path = require('path');
const STATE_FILE = path.join(os.tmpdir(), 'spine-thumbnail-progress.json');
const ALIVE_FILE = path.join(os.tmpdir(), 'spine-thumbnail-progress-alive');
const CONTROL_FILE = path.join(os.tmpdir(), 'spine-thumbnail-progress-control.json');
const WIDTH = 380;
const bar = document.getElementById('bar');
const countEl = document.getElementById('count');
const nameEl = document.getElementById('name');
const actionEl = document.getElementById('action');
const confirmEl = document.getElementById('confirm');
const confirmTextEl = document.getElementById('confirmText');
const resumeEl = document.getElementById('resume');
let finished = false;
let paused = false;
let doneCount = 0;
let lastHeight = 0;
let placedAt = null; // 上次由我们设置的位置,用来判断用户是不是手动拖过
/** Eagle 的 userData 目录;eagle.app.userDataPath 注入得比较晚,所以留一个兜底。 */
function userDataPath() {
if (eagle.app && eagle.app.userDataPath) return eagle.app.userDataPath;
return path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'Eagle');
}
/** 算出"Eagle 主窗口正中心"对应的弹窗位置;读不到就返回 null。 */
function centerTarget(width, height) {
let state = null;
try {
state = JSON.parse(fs.readFileSync(path.join(userDataPath(), 'window-state.json'), 'utf8'));
} catch (error) { /* 读不到就用不了 */ }
if (!state || !Number.isFinite(state.x)) return null;
// 主窗口最大化/全屏时记录的是还原后的位置,拿它当基准会偏,改用所在显示器居中。
const area = (state.isMaximized || state.isFullScreen) && state.displayBounds ? state.displayBounds : state;
return [
Math.round(area.x + (area.width - width) / 2),
Math.round(area.y + (area.height - height) / 2),
];
}
/** 跟着 Eagle 主窗口居中;一旦用户手动拖过窗口,就只改尺寸、不再动位置。 */
async function reCenter(width, height) {
try {
const current = await eagle.window.getPosition();
const movedByUser = placedAt && (Math.abs(current[0] - placedAt[0]) > 4 || Math.abs(current[1] - placedAt[1]) > 4);
if (movedByUser) return;
} catch (error) { /* 拿不到位置就照常居中 */ }
const target = centerTarget(width, height);
if (!target) {
await eagle.window.center();
return;
}
placedAt = target;
await eagle.window.setPosition(target[0], target[1]);
}
/** 窗口高度跟着内容走。 */
function fitHeight() {
const height = Math.ceil(document.body.scrollHeight);
if (Math.abs(height - lastHeight) < 2) return;
lastHeight = height;
eagle.window.setSize(WIDTH, height)
.then(() => reCenter(WIDTH, height))
.catch(() => { /* 调整失败不影响显示 */ });
}
/** 三种状态:生成中显示「停止」,暂停询问时显示「继续 / 取消」,跑完显示「确认」。 */
function syncButtons() {
actionEl.textContent = finished ? '确认' : '停止';
actionEl.style.display = paused ? 'none' : '';
confirmEl.style.display = paused ? 'flex' : 'none';
fitHeight();
}
function writeControl(action) {
try {
fs.writeFileSync(CONTROL_FILE, JSON.stringify({ action, at: Date.now() }), 'utf8');
} catch (error) { /* 写不进去就等于没按 */ }
}
function render(state) {
const total = Math.max(1, state.total || 0);
const done = state.done || 0;
finished = state.finished === true;
if (finished) paused = false;
doneCount = done;
confirmTextEl.textContent = '已生成 ' + doneCount + ' 张缩略图,是否确认取消?';
if (finished) {
bar.style.width = '100%';
countEl.textContent = state.text || '已完成';
nameEl.textContent = '';
} else {
bar.style.width = Math.min(100, done / total * 100) + '%';
countEl.textContent = '已完成 ' + done + ' / ' + (state.total || 0) +
'(' + Math.round(done / total * 100) + '%)';
nameEl.textContent = state.name || '';
}
syncButtons();
}
actionEl.addEventListener('click', () => {
if (finished) {
window.close();
return;
}
// 先让宿主暂停(它会在当前素材做完后停在原地),再就地问要不要继续。
paused = true;
writeControl('pause');
syncButtons();
resumeEl.focus(); // 默认选中「继续」
});
resumeEl.addEventListener('click', () => {
paused = false;
writeControl('resume');
syncButtons();
});
document.getElementById('cancel').addEventListener('click', () => {
// 同步写控制文件再关窗:窗口没了宿主也能读到 cancel,把剩余队列丢掉。
writeControl('cancel');
window.close();
});
// 窗口尺寸/置顶/居中:Eagle 的 window-state.json 会覆盖 manifest 里的位置和宽高,这里再对齐一次。
(async () => {
try {
await eagle.window.setResizable(false);
} catch (error) { /* ignore */ }
syncButtons();
})();
// 心跳:让宿主知道这个窗口还开着;窗口关掉后心跳就停了,下次需要时会重新开。
const beat = () => {
try { fs.writeFileSync(ALIVE_FILE, String(Date.now()), 'utf8'); } catch (error) { /* ignore */ }
};
beat();
setInterval(beat, 1000);
let lastStamp = 0;
setInterval(() => {
let state;
try {
state = JSON.parse(fs.readFileSync(STATE_FILE, 'utf8'));
} catch (error) {
return; // 状态文件还没写出来
}
if (state.updatedAt === lastStamp) return;
lastStamp = state.updatedAt;
render(state);
}, 400);
</script>
</body>
</html>