干净发布副本
This commit is contained in:
1 parent
c29de7a26b
commit
acc93d5404
35 files changed
+82427
No files matched your search
@@ -0,0 +1,219 @@
|
||||
<!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; // 上次由我们设置的位置,用来判断用户是不是手动拖过
|
||||
let allowClose = false;
|
||||
|
||||
/** Eagle 的 userData 目录;eagle.app.userDataPath 注入得比较晚,所以留一个兜底。 */
|
||||
function userDataPath() {
|
||||
if (eagle.app && eagle.app.userDataPath) return eagle.app.userDataPath;
|
||||
if (process.platform === 'darwin') return path.join(os.homedir(), 'Library', 'Application Support', 'Eagle');
|
||||
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() {
|
||||
// setSize 设置的是包含系统标题栏的外部高度,内容高度要补回窗口边框占用。
|
||||
const height = Math.ceil(document.body.scrollHeight + Math.max(0, window.outerHeight - window.innerHeight));
|
||||
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 requestStop() {
|
||||
if (paused) return;
|
||||
paused = true;
|
||||
writeControl('pause');
|
||||
syncButtons();
|
||||
resumeEl.focus();
|
||||
}
|
||||
|
||||
// 系统关闭按钮与页面里的「停止」走同一条暂停确认流程。
|
||||
window.addEventListener('beforeunload', (event) => {
|
||||
if (allowClose || finished) return;
|
||||
event.preventDefault();
|
||||
event.returnValue = false;
|
||||
requestStop();
|
||||
});
|
||||
|
||||
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) {
|
||||
allowClose = true;
|
||||
window.close();
|
||||
return;
|
||||
}
|
||||
requestStop();
|
||||
});
|
||||
|
||||
resumeEl.addEventListener('click', () => {
|
||||
paused = false;
|
||||
writeControl('resume');
|
||||
syncButtons();
|
||||
});
|
||||
|
||||
document.getElementById('cancel').addEventListener('click', () => {
|
||||
// 同步写控制文件再关窗:窗口没了宿主也能读到 cancel,把剩余队列丢掉。
|
||||
writeControl('cancel');
|
||||
allowClose = true;
|
||||
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>
|
||||
Reference in new issue
Block a user